commit
368c5d10ee
|
@ -23,6 +23,8 @@ from subiquity.models.filesystem import (
|
||||||
LVM_VolGroup,
|
LVM_VolGroup,
|
||||||
Partition,
|
Partition,
|
||||||
Raid,
|
Raid,
|
||||||
|
ZFS,
|
||||||
|
ZPool,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,6 +156,11 @@ def _desc_gap(gap):
|
||||||
return _("to gap")
|
return _("to gap")
|
||||||
|
|
||||||
|
|
||||||
|
@desc.register(ZPool)
|
||||||
|
def _desc_zpool(zpool):
|
||||||
|
return _("zpool")
|
||||||
|
|
||||||
|
|
||||||
@functools.singledispatch
|
@functools.singledispatch
|
||||||
def label(device, *, short=False):
|
def label(device, *, short=False):
|
||||||
"""A label that identifies `device`
|
"""A label that identifies `device`
|
||||||
|
@ -336,3 +343,19 @@ def _for_client_partition(partition, *, min_size=0):
|
||||||
@for_client.register(gaps.Gap)
|
@for_client.register(gaps.Gap)
|
||||||
def _for_client_gap(gap, *, min_size=0):
|
def _for_client_gap(gap, *, min_size=0):
|
||||||
return types.Gap(offset=gap.offset, size=gap.size, usable=gap.usable)
|
return types.Gap(offset=gap.offset, size=gap.size, usable=gap.usable)
|
||||||
|
|
||||||
|
|
||||||
|
@for_client.register(ZPool)
|
||||||
|
def _for_client_zpool(zpool, *, min_size=0):
|
||||||
|
return types.ZPool(
|
||||||
|
pool=zpool.pool,
|
||||||
|
mountpoint=zpool.mountpoint,
|
||||||
|
zfses=[for_client(zfs) for zfs in zpool.zfses],
|
||||||
|
pool_properties=zpool.pool_properties,
|
||||||
|
fs_properties=zpool.fs_properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@for_client.register(ZFS)
|
||||||
|
def _for_client_zfs(zfs, *, min_size=0):
|
||||||
|
return types.ZFS(volume=zfs.volume, properties=zfs.properties)
|
||||||
|
|
|
@ -157,6 +157,9 @@ class FilesystemManipulator:
|
||||||
self.model.remove_logical_volume(lv)
|
self.model.remove_logical_volume(lv)
|
||||||
delete_lvm_partition = delete_logical_volume
|
delete_lvm_partition = delete_logical_volume
|
||||||
|
|
||||||
|
def create_zpool(self, device, pool, mountpoint):
|
||||||
|
self.model.add_zpool(device, pool, mountpoint)
|
||||||
|
|
||||||
def delete(self, obj):
|
def delete(self, obj):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return
|
return
|
||||||
|
|
|
@ -15,7 +15,10 @@
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from subiquity.common.types import SizingPolicy
|
from subiquity.common.types import (
|
||||||
|
GuidedCapability,
|
||||||
|
SizingPolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestSizingPolicy(unittest.TestCase):
|
class TestSizingPolicy(unittest.TestCase):
|
||||||
|
@ -30,3 +33,11 @@ class TestSizingPolicy(unittest.TestCase):
|
||||||
def test_default(self):
|
def test_default(self):
|
||||||
actual = SizingPolicy.from_string(None)
|
actual = SizingPolicy.from_string(None)
|
||||||
self.assertEqual(SizingPolicy.SCALED, actual)
|
self.assertEqual(SizingPolicy.SCALED, actual)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCapabilities(unittest.TestCase):
|
||||||
|
def test_not_zfs(self):
|
||||||
|
self.assertFalse(GuidedCapability.DIRECT.is_zfs())
|
||||||
|
|
||||||
|
def test_is_zfs(self):
|
||||||
|
self.assertTrue(GuidedCapability.ZFS.is_zfs())
|
||||||
|
|
|
@ -290,6 +290,21 @@ class Partition:
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class ZFS:
|
||||||
|
volume: str
|
||||||
|
properties: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class ZPool:
|
||||||
|
pool: str
|
||||||
|
mountpoint: str
|
||||||
|
zfses: Optional[ZFS] = None
|
||||||
|
pool_properties: Optional[dict] = None
|
||||||
|
fs_properties: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class GapUsable(enum.Enum):
|
class GapUsable(enum.Enum):
|
||||||
YES = enum.auto()
|
YES = enum.auto()
|
||||||
TOO_MANY_PRIMARY_PARTS = enum.auto()
|
TOO_MANY_PRIMARY_PARTS = enum.auto()
|
||||||
|
@ -325,6 +340,8 @@ class GuidedCapability(enum.Enum):
|
||||||
DIRECT = enum.auto()
|
DIRECT = enum.auto()
|
||||||
LVM = enum.auto()
|
LVM = enum.auto()
|
||||||
LVM_LUKS = enum.auto()
|
LVM_LUKS = enum.auto()
|
||||||
|
ZFS = enum.auto()
|
||||||
|
|
||||||
CORE_BOOT_ENCRYPTED = enum.auto()
|
CORE_BOOT_ENCRYPTED = enum.auto()
|
||||||
CORE_BOOT_UNENCRYPTED = enum.auto()
|
CORE_BOOT_UNENCRYPTED = enum.auto()
|
||||||
# These two are not valid as GuidedChoiceV2.capability:
|
# These two are not valid as GuidedChoiceV2.capability:
|
||||||
|
@ -349,6 +366,9 @@ class GuidedCapability(enum.Enum):
|
||||||
GuidedCapability.LVM,
|
GuidedCapability.LVM,
|
||||||
GuidedCapability.LVM_LUKS]
|
GuidedCapability.LVM_LUKS]
|
||||||
|
|
||||||
|
def is_zfs(self) -> bool:
|
||||||
|
return self in [GuidedCapability.ZFS]
|
||||||
|
|
||||||
|
|
||||||
class GuidedDisallowedCapabilityReason(enum.Enum):
|
class GuidedDisallowedCapabilityReason(enum.Enum):
|
||||||
TOO_SMALL = enum.auto()
|
TOO_SMALL = enum.auto()
|
||||||
|
|
|
@ -1039,10 +1039,13 @@ class Filesystem:
|
||||||
class Mount:
|
class Mount:
|
||||||
path: str
|
path: str
|
||||||
device: Filesystem = attributes.ref(backlink="_mount", default=None)
|
device: Filesystem = attributes.ref(backlink="_mount", default=None)
|
||||||
fstype: Optional[str] = None
|
|
||||||
options: Optional[str] = None
|
options: Optional[str] = None
|
||||||
spec: Optional[str] = None
|
spec: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fstype(self):
|
||||||
|
return self.device.fstype
|
||||||
|
|
||||||
def can_delete(self):
|
def can_delete(self):
|
||||||
from subiquity.common.filesystem import boot
|
from subiquity.common.filesystem import boot
|
||||||
# Can't delete mount of /boot/efi or swap, anything else is fine.
|
# Can't delete mount of /boot/efi or swap, anything else is fine.
|
||||||
|
@ -1072,6 +1075,20 @@ class ZPool:
|
||||||
# default dataset options for the zfses in the pool
|
# default dataset options for the zfses in the pool
|
||||||
fs_properties: Optional[dict] = None
|
fs_properties: Optional[dict] = None
|
||||||
|
|
||||||
|
component_name = "vdev"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fstype(self):
|
||||||
|
return 'zfs'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.pool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mount(self):
|
||||||
|
return self.mountpoint
|
||||||
|
|
||||||
async def pre_shutdown(self, command_runner):
|
async def pre_shutdown(self, command_runner):
|
||||||
await command_runner.run(['zpool', 'export', self.pool])
|
await command_runner.run(['zpool', 'export', self.pool])
|
||||||
|
|
||||||
|
@ -1831,9 +1848,28 @@ class FilesystemModel(object):
|
||||||
def should_add_swapfile(self):
|
def should_add_swapfile(self):
|
||||||
mount = self._mount_for_path('/')
|
mount = self._mount_for_path('/')
|
||||||
if mount is not None:
|
if mount is not None:
|
||||||
if not can_use_swapfile('/', mount.device.fstype):
|
if not can_use_swapfile('/', mount.fstype):
|
||||||
return False
|
return False
|
||||||
for swap in self._all(type='format', fstype='swap'):
|
for swap in self._all(type='format', fstype='swap'):
|
||||||
if swap.mount():
|
if swap.mount():
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def add_zpool(self, device, pool, mountpoint):
|
||||||
|
fs_properties = dict(
|
||||||
|
acltype='posixacl',
|
||||||
|
relatime='on',
|
||||||
|
canmount='on',
|
||||||
|
compression='gzip',
|
||||||
|
devices='off',
|
||||||
|
xattr='sa',
|
||||||
|
)
|
||||||
|
zpool = ZPool(
|
||||||
|
m=self,
|
||||||
|
vdevs=[device],
|
||||||
|
pool=pool,
|
||||||
|
mountpoint=mountpoint,
|
||||||
|
pool_properties=dict(ashift=12),
|
||||||
|
fs_properties=fs_properties)
|
||||||
|
self._actions.append(zpool)
|
||||||
|
return zpool
|
||||||
|
|
|
@ -198,6 +198,7 @@ class VariationInfo:
|
||||||
GuidedCapability.DIRECT,
|
GuidedCapability.DIRECT,
|
||||||
GuidedCapability.LVM,
|
GuidedCapability.LVM,
|
||||||
GuidedCapability.LVM_LUKS,
|
GuidedCapability.LVM_LUKS,
|
||||||
|
GuidedCapability.ZFS,
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
@ -472,6 +473,18 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
||||||
mount="/",
|
mount="/",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def guided_zfs(self, gap, choice: GuidedChoiceV2):
|
||||||
|
device = gap.device
|
||||||
|
part_align = device.alignment_data().part_align
|
||||||
|
bootfs_size = align_up(sizes.get_bootfs_size(gap.size), part_align)
|
||||||
|
gap_boot, gap_rest = gap.split(bootfs_size)
|
||||||
|
|
||||||
|
bpool_part = self.create_partition(device, gap_boot, dict(fstype=None))
|
||||||
|
rpool_part = self.create_partition(device, gap_rest, dict(fstype=None))
|
||||||
|
|
||||||
|
self.create_zpool(rpool_part, 'rpool', '/')
|
||||||
|
self.create_zpool(bpool_part, 'bpool', '/boot')
|
||||||
|
|
||||||
@functools.singledispatchmethod
|
@functools.singledispatchmethod
|
||||||
def start_guided(self, target: GuidedStorageTarget,
|
def start_guided(self, target: GuidedStorageTarget,
|
||||||
disk: ModelDisk) -> gaps.Gap:
|
disk: ModelDisk) -> gaps.Gap:
|
||||||
|
@ -589,6 +602,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
||||||
|
|
||||||
if choice.capability.is_lvm():
|
if choice.capability.is_lvm():
|
||||||
self.guided_lvm(gap, choice)
|
self.guided_lvm(gap, choice)
|
||||||
|
elif choice.capability.is_zfs():
|
||||||
|
self.guided_zfs(gap, choice)
|
||||||
elif choice.capability == GuidedCapability.DIRECT:
|
elif choice.capability == GuidedCapability.DIRECT:
|
||||||
self.guided_direct(gap)
|
self.guided_direct(gap)
|
||||||
else:
|
else:
|
||||||
|
@ -1186,6 +1201,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
||||||
capability = GuidedCapability.LVM_LUKS
|
capability = GuidedCapability.LVM_LUKS
|
||||||
else:
|
else:
|
||||||
capability = GuidedCapability.LVM
|
capability = GuidedCapability.LVM
|
||||||
|
elif name == 'zfs':
|
||||||
|
capability = GuidedCapability.ZFS
|
||||||
else:
|
else:
|
||||||
capability = GuidedCapability.DIRECT
|
capability = GuidedCapability.DIRECT
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ default_capabilities = [
|
||||||
GuidedCapability.DIRECT,
|
GuidedCapability.DIRECT,
|
||||||
GuidedCapability.LVM,
|
GuidedCapability.LVM,
|
||||||
GuidedCapability.LVM_LUKS,
|
GuidedCapability.LVM_LUKS,
|
||||||
|
GuidedCapability.ZFS,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -459,6 +460,41 @@ class TestGuided(IsolatedAsyncioTestCase):
|
||||||
self.assertFalse(d1p2.preserve)
|
self.assertFalse(d1p2.preserve)
|
||||||
self.assertIsNone(gaps.largest_gap(self.d1))
|
self.assertIsNone(gaps.largest_gap(self.d1))
|
||||||
|
|
||||||
|
@parameterized.expand(boot_expectations)
|
||||||
|
async def test_guided_zfs(self, bootloader, ptable, p1mnt):
|
||||||
|
await self._guided_setup(bootloader, ptable)
|
||||||
|
target = GuidedStorageTargetReformat(
|
||||||
|
disk_id=self.d1.id, allowed=default_capabilities)
|
||||||
|
await self.controller.guided(GuidedChoiceV2(
|
||||||
|
target=target, capability=GuidedCapability.ZFS))
|
||||||
|
[d1p1, d1p2, d1p3] = self.d1.partitions()
|
||||||
|
self.assertEqual(p1mnt, d1p1.mount)
|
||||||
|
self.assertEqual(None, d1p2.mount)
|
||||||
|
self.assertEqual(None, d1p3.mount)
|
||||||
|
self.assertFalse(d1p1.preserve)
|
||||||
|
self.assertFalse(d1p2.preserve)
|
||||||
|
self.assertFalse(d1p3.preserve)
|
||||||
|
[rpool] = self.model._all(type='zpool', pool='rpool')
|
||||||
|
self.assertEqual('/', rpool.mount)
|
||||||
|
[bpool] = self.model._all(type='zpool', pool='bpool')
|
||||||
|
self.assertEqual('/boot', bpool.mount)
|
||||||
|
|
||||||
|
async def test_guided_zfs_BIOS_MSDOS(self):
|
||||||
|
await self._guided_setup(Bootloader.BIOS, 'msdos')
|
||||||
|
target = GuidedStorageTargetReformat(
|
||||||
|
disk_id=self.d1.id, allowed=default_capabilities)
|
||||||
|
await self.controller.guided(GuidedChoiceV2(
|
||||||
|
target=target, capability=GuidedCapability.ZFS))
|
||||||
|
[d1p1, d1p2] = self.d1.partitions()
|
||||||
|
self.assertEqual(None, d1p1.mount)
|
||||||
|
self.assertEqual(None, d1p2.mount)
|
||||||
|
self.assertFalse(d1p1.preserve)
|
||||||
|
self.assertFalse(d1p2.preserve)
|
||||||
|
[rpool] = self.model._all(type='zpool', pool='rpool')
|
||||||
|
self.assertEqual('/', rpool.mount)
|
||||||
|
[bpool] = self.model._all(type='zpool', pool='bpool')
|
||||||
|
self.assertEqual('/boot', bpool.mount)
|
||||||
|
|
||||||
async def _guided_side_by_side(self, bl, ptable):
|
async def _guided_side_by_side(self, bl, ptable):
|
||||||
await self._guided_setup(bl, ptable, storage_version=2)
|
await self._guided_setup(bl, ptable, storage_version=2)
|
||||||
self.controller.add_boot_disk(self.d1)
|
self.controller.add_boot_disk(self.d1)
|
||||||
|
@ -618,11 +654,8 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
|
||||||
disabled_cap.capability
|
disabled_cap.capability
|
||||||
for disabled_cap in resp.targets[0].disallowed
|
for disabled_cap in resp.targets[0].disallowed
|
||||||
},
|
},
|
||||||
{
|
set(default_capabilities)
|
||||||
GuidedCapability.DIRECT,
|
)
|
||||||
GuidedCapability.LVM,
|
|
||||||
GuidedCapability.LVM_LUKS,
|
|
||||||
})
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{
|
{
|
||||||
disabled_cap.reason
|
disabled_cap.reason
|
||||||
|
|
Loading…
Reference in New Issue