diff --git a/subiquity/common/filesystem/labels.py b/subiquity/common/filesystem/labels.py index 0ac5c07ae..05b37f227 100644 --- a/subiquity/common/filesystem/labels.py +++ b/subiquity/common/filesystem/labels.py @@ -23,6 +23,8 @@ LVM_VolGroup, Partition, Raid, + ZFS, + ZPool, ) @@ -154,6 +156,11 @@ def _desc_gap(gap): return _("to gap") +@desc.register(ZPool) +def _desc_zpool(zpool): + return _("zpool") + + @functools.singledispatch def label(device, *, short=False): """A label that identifies `device` @@ -336,3 +343,19 @@ def _for_client_partition(partition, *, min_size=0): @for_client.register(gaps.Gap) def _for_client_gap(gap, *, min_size=0): 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) diff --git a/subiquity/common/filesystem/manipulator.py b/subiquity/common/filesystem/manipulator.py index e23f3b2ab..7980746a7 100644 --- a/subiquity/common/filesystem/manipulator.py +++ b/subiquity/common/filesystem/manipulator.py @@ -157,6 +157,9 @@ def delete_logical_volume(self, lv): self.model.remove_logical_volume(lv) delete_lvm_partition = delete_logical_volume + def create_zpool(self, device, pool, mountpoint): + self.model.add_zpool(device, pool, mountpoint) + def delete(self, obj): if obj is None: return diff --git a/subiquity/common/tests/test_types.py b/subiquity/common/tests/test_types.py index cc59e4fcb..5974a7da6 100644 --- a/subiquity/common/tests/test_types.py +++ b/subiquity/common/tests/test_types.py @@ -15,7 +15,10 @@ import unittest -from subiquity.common.types import SizingPolicy +from subiquity.common.types import ( + GuidedCapability, + SizingPolicy, +) class TestSizingPolicy(unittest.TestCase): @@ -30,3 +33,11 @@ def test_scaled_size(self): def test_default(self): actual = SizingPolicy.from_string(None) 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()) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 8ed558e26..623d569cf 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -290,6 +290,21 @@ class Partition: 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): YES = enum.auto() TOO_MANY_PRIMARY_PARTS = enum.auto() @@ -325,6 +340,8 @@ class GuidedCapability(enum.Enum): DIRECT = enum.auto() LVM = enum.auto() LVM_LUKS = enum.auto() + ZFS = enum.auto() + CORE_BOOT_ENCRYPTED = enum.auto() CORE_BOOT_UNENCRYPTED = enum.auto() # These two are not valid as GuidedChoiceV2.capability: @@ -349,6 +366,9 @@ def supports_manual_customization(self) -> bool: GuidedCapability.LVM, GuidedCapability.LVM_LUKS] + def is_zfs(self) -> bool: + return self in [GuidedCapability.ZFS] + class GuidedDisallowedCapabilityReason(enum.Enum): TOO_SMALL = enum.auto() diff --git a/subiquity/models/filesystem.py b/subiquity/models/filesystem.py index 23521a1a5..140451642 100644 --- a/subiquity/models/filesystem.py +++ b/subiquity/models/filesystem.py @@ -1039,10 +1039,13 @@ def _available(self): class Mount: path: str device: Filesystem = attributes.ref(backlink="_mount", default=None) - fstype: Optional[str] = None options: Optional[str] = None spec: Optional[str] = None + @property + def fstype(self): + return self.device.fstype + def can_delete(self): from subiquity.common.filesystem import boot # 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 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): await command_runner.run(['zpool', 'export', self.pool]) @@ -1831,9 +1848,28 @@ def can_install(self): def should_add_swapfile(self): mount = self._mount_for_path('/') if mount is not None: - if not can_use_swapfile('/', mount.device.fstype): + if not can_use_swapfile('/', mount.fstype): return False for swap in self._all(type='format', fstype='swap'): if swap.mount(): return False 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 diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index 7e8d3d255..09a5f5d46 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -198,6 +198,7 @@ def classic(cls, name: str, min_size: int): GuidedCapability.DIRECT, GuidedCapability.LVM, GuidedCapability.LVM_LUKS, + GuidedCapability.ZFS, ])) @@ -472,6 +473,18 @@ def guided_lvm(self, gap, choice: GuidedChoiceV2): 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 def start_guided(self, target: GuidedStorageTarget, disk: ModelDisk) -> gaps.Gap: @@ -589,6 +602,8 @@ async def guided( if choice.capability.is_lvm(): self.guided_lvm(gap, choice) + elif choice.capability.is_zfs(): + self.guided_zfs(gap, choice) elif choice.capability == GuidedCapability.DIRECT: self.guided_direct(gap) else: @@ -1186,6 +1201,8 @@ async def run_autoinstall_guided(self, layout): capability = GuidedCapability.LVM_LUKS else: capability = GuidedCapability.LVM + elif name == 'zfs': + capability = GuidedCapability.ZFS else: capability = GuidedCapability.DIRECT diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py index db3c70843..4241f756b 100644 --- a/subiquity/server/controllers/tests/test_filesystem.py +++ b/subiquity/server/controllers/tests/test_filesystem.py @@ -72,6 +72,7 @@ GuidedCapability.DIRECT, GuidedCapability.LVM, GuidedCapability.LVM_LUKS, + GuidedCapability.ZFS, ] @@ -459,6 +460,41 @@ async def test_guided_lvm_BIOS_MSDOS(self): self.assertFalse(d1p2.preserve) 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): await self._guided_setup(bl, ptable, storage_version=2) self.controller.add_boot_disk(self.d1) @@ -618,11 +654,8 @@ async def test_small_blank_disk(self, bootloader, ptable): disabled_cap.capability for disabled_cap in resp.targets[0].disallowed }, - { - GuidedCapability.DIRECT, - GuidedCapability.LVM, - GuidedCapability.LVM_LUKS, - }) + set(default_capabilities) + ) self.assertEqual( { disabled_cap.reason