Merge pull request #1699 from dbungert/zfs-guided

Zfs guided
This commit is contained in:
Dan Bungert 2023-07-18 08:24:07 -06:00 committed by GitHub
commit 368c5d10ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 8 deletions

View File

@ -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)

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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