diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 2bffefa2..896b5f73 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -386,6 +386,12 @@ class StorageResponseV2: install_minimum_size: Optional[int] = None +class GuidedCapability(enum.Enum): + DIRECT = enum.auto() + LVM = enum.auto() + LVM_LUKS = enum.auto() + + @attr.s(auto_attribs=True) class GuidedResizeValues: install_max: int @@ -397,6 +403,7 @@ class GuidedResizeValues: @attr.s(auto_attribs=True) class GuidedStorageTargetReformat: disk_id: str + capabilities: List[GuidedCapability] @attr.s(auto_attribs=True) @@ -407,9 +414,10 @@ class GuidedStorageTargetResize: minimum: Optional[int] recommended: Optional[int] maximum: Optional[int] + capabilities: List[GuidedCapability] @staticmethod - def from_recommendations(part, resize_vals): + def from_recommendations(part, resize_vals, capabilities): return GuidedStorageTargetResize( disk_id=part.device.id, partition_number=part.number, @@ -417,6 +425,7 @@ class GuidedStorageTargetResize: minimum=resize_vals.minimum, recommended=resize_vals.recommended, maximum=resize_vals.maximum, + capabilities=capabilities, ) @@ -424,6 +433,7 @@ class GuidedStorageTargetResize: class GuidedStorageTargetUseGap: disk_id: str gap: Gap + capabilities: List[GuidedCapability] GuidedStorageTarget = Union[GuidedStorageTargetReformat, @@ -434,14 +444,22 @@ GuidedStorageTarget = Union[GuidedStorageTargetReformat, @attr.s(auto_attribs=True) class GuidedChoiceV2: target: GuidedStorageTarget - use_lvm: bool = False + capability: GuidedCapability password: Optional[str] = attr.ib(default=None, repr=False) @staticmethod def from_guided_choice(choice: GuidedChoice): + if choice.use_lvm: + if choice.password is not None: + capability = GuidedCapability.LVM_LUKS + else: + capability = GuidedCapability.LVM + else: + capability = GuidedCapability.DIRECT return GuidedChoiceV2( - target=GuidedStorageTargetReformat(disk_id=choice.disk_id), - use_lvm=choice.use_lvm, + target=GuidedStorageTargetReformat( + disk_id=choice.disk_id, capabilities=[capability]), + capability=capability, password=choice.password, ) diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index 5c3179b8..280bdc84 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -58,6 +58,7 @@ from subiquity.common.types import ( AddPartitionV2, Bootloader, Disk, + GuidedCapability, GuidedChoice, GuidedChoiceV2, GuidedStorageResponse, @@ -355,15 +356,12 @@ class FilesystemController(SubiquityController, FilesystemManipulator): return gap def build_lvm_options(self, passphrase): - if passphrase is None: - return None - else: - return { - 'encrypt': True, - 'luks_options': { - 'passphrase': passphrase, - }, - } + return { + 'encrypt': True, + 'luks_options': { + 'passphrase': passphrase, + }, + } def guided(self, choice: GuidedChoiceV2): self.model.guided_configuration = choice @@ -377,11 +375,15 @@ class FilesystemController(SubiquityController, FilesystemManipulator): if gap is None: raise Exception('failed to locate gap after adding boot') - if choice.use_lvm: + if choice.capability == GuidedCapability.LVM: + self.guided_lvm(gap, lvm_options=None) + elif choice.capability == GuidedCapability.LVM_LUKS: lvm_options = self.build_lvm_options(choice.password) self.guided_lvm(gap, lvm_options=lvm_options) - else: + elif choice.capability == GuidedCapability.DIRECT: self.guided_direct(gap) + else: + raise ValueError('cannot process capability') async def _probe_response(self, wait, resp_cls): if not self._probe_task.done(): @@ -683,9 +685,16 @@ class FilesystemController(SubiquityController, FilesystemManipulator): scenarios = [] install_min = self.calculate_suggested_install_min() + capabilities = [ + GuidedCapability.DIRECT, + GuidedCapability.LVM, + GuidedCapability.LVM_LUKS + ] + for disk in self.potential_boot_disks(with_reformatting=True): if disk.size >= install_min: - reformat = GuidedStorageTargetReformat(disk_id=disk.id) + reformat = GuidedStorageTargetReformat( + disk_id=disk.id, capabilities=capabilities) scenarios.append((disk.size, reformat)) for disk in self.potential_boot_disks(with_reformatting=False): @@ -697,8 +706,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator): if gap is not None and gap.size >= install_min: api_gap = labels.for_client(gap) use_gap = GuidedStorageTargetUseGap( - disk_id=disk.id, - gap=api_gap) + disk_id=disk.id, gap=api_gap, capabilities=capabilities) scenarios.append((gap.size, use_gap)) for disk in self.potential_boot_disks(check_boot=False): @@ -714,14 +722,14 @@ class FilesystemController(SubiquityController, FilesystemManipulator): with_reformatting=False): continue resize = GuidedStorageTargetResize.from_recommendations( - partition, vals) + partition, vals, capabilities=capabilities) scenarios.append((vals.install_max, resize)) scenarios.sort(reverse=True, key=lambda x: x[0]) return GuidedStorageResponseV2( status=ProbeStatus.DONE, configured=self.model.guided_configuration, - possible=[s[1] for s in scenarios]) + possible=[s[1] for s in scenarios if s[1].capabilities]) async def v2_guided_POST(self, data: GuidedChoiceV2) \ -> GuidedStorageResponseV2: @@ -907,7 +915,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator): if mode == 'reformat_disk': match = layout.get("match", {'size': 'largest'}) disk = self.model.disk_for_match(self.model.all_disks(), match) - target = GuidedStorageTargetReformat(disk_id=disk.id) + target = GuidedStorageTargetReformat( + disk_id=disk.id, capabilities=[]) elif mode == 'use_gap': bootable = [d for d in self.model.all_disks() if boot.can_be_boot_device(d, with_reformatting=False)] @@ -915,13 +924,21 @@ class FilesystemController(SubiquityController, FilesystemManipulator): if not gap: raise Exception("autoinstall cannot configure storage " "- no gap found large enough for install") - target = GuidedStorageTargetUseGap(disk_id=gap.device.id, gap=gap) + target = GuidedStorageTargetUseGap( + disk_id=gap.device.id, gap=gap, capabilities=[]) log.info(f'autoinstall: running guided {name} install in mode {mode} ' f'using {target}') - use_lvm = name == 'lvm' password = layout.get('password', None) - self.guided(GuidedChoiceV2(target=target, use_lvm=use_lvm, + if name == 'lvm': + if password is not None: + capability = GuidedCapability.LVM_LUKS + else: + capability = GuidedCapability.LVM + else: + capability = GuidedCapability.DIRECT + password = layout.get('password', None) + self.guided(GuidedChoiceV2(target=target, capability=capability, password=password)) def validate_layout_mode(self, mode): diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py index 793fa3f2..7d12b775 100644 --- a/subiquity/server/controllers/tests/test_filesystem.py +++ b/subiquity/server/controllers/tests/test_filesystem.py @@ -26,6 +26,7 @@ from subiquitycore.tests.util import random_string from subiquity.common.filesystem import gaps from subiquity.common.types import ( Bootloader, + GuidedCapability, GuidedChoiceV2, GuidedStorageTargetReformat, GuidedStorageTargetResize, @@ -47,6 +48,13 @@ bootloaders_and_ptables = [(bl, pt) for pt in ('gpt', 'msdos', 'vtoc')] +default_capabilities = [ + GuidedCapability.DIRECT, + GuidedCapability.LVM, + GuidedCapability.LVM_LUKS, + ] + + class TestSubiquityControllerFilesystem(IsolatedAsyncioTestCase): def setUp(self): self.app = make_app() @@ -98,8 +106,10 @@ class TestGuided(IsolatedAsyncioTestCase): @parameterized.expand(boot_expectations) async def test_guided_direct(self, bootloader, ptable, p1mnt): self._guided_setup(bootloader, ptable) - target = GuidedStorageTargetReformat(disk_id=self.d1.id) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=False)) + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, capabilities=default_capabilities) + self.controller.guided( + GuidedChoiceV2(target=target, capability=GuidedCapability.DIRECT)) [d1p1, d1p2] = self.d1.partitions() self.assertEqual(p1mnt, d1p1.mount) self.assertEqual('/', d1p2.mount) @@ -109,8 +119,10 @@ class TestGuided(IsolatedAsyncioTestCase): async def test_guided_direct_BIOS_MSDOS(self): self._guided_setup(Bootloader.BIOS, 'msdos') - target = GuidedStorageTargetReformat(disk_id=self.d1.id) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=False)) + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, capabilities=default_capabilities) + self.controller.guided( + GuidedChoiceV2(target=target, capability=GuidedCapability.DIRECT)) [d1p1] = self.d1.partitions() self.assertEqual('/', d1p1.mount) self.assertFalse(d1p1.preserve) @@ -119,8 +131,10 @@ class TestGuided(IsolatedAsyncioTestCase): @parameterized.expand(boot_expectations) async def test_guided_lvm(self, bootloader, ptable, p1mnt): self._guided_setup(bootloader, ptable) - target = GuidedStorageTargetReformat(disk_id=self.d1.id) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=True)) + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, capabilities=default_capabilities) + self.controller.guided(GuidedChoiceV2( + target=target, capability=GuidedCapability.LVM)) [d1p1, d1p2, d1p3] = self.d1.partitions() self.assertEqual(p1mnt, d1p1.mount) self.assertEqual('/boot', d1p2.mount) @@ -135,8 +149,10 @@ class TestGuided(IsolatedAsyncioTestCase): async def test_guided_lvm_BIOS_MSDOS(self): self._guided_setup(Bootloader.BIOS, 'msdos') - target = GuidedStorageTargetReformat(disk_id=self.d1.id) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=True)) + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, capabilities=default_capabilities) + self.controller.guided( + GuidedChoiceV2(target=target, capability=GuidedCapability.LVM)) [d1p1, d1p2] = self.d1.partitions() self.assertEqual('/boot', d1p1.mount) [vg] = self.model._all(type='lvm_volgroup') @@ -182,8 +198,10 @@ class TestGuided(IsolatedAsyncioTestCase): self._guided_side_by_side(bl, pt) parts_before = self.d1._partitions.copy() gap = gaps.largest_gap(self.d1) - target = GuidedStorageTargetUseGap(disk_id=self.d1.id, gap=gap) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=False)) + target = GuidedStorageTargetUseGap( + disk_id=self.d1.id, gap=gap, capabilities=default_capabilities) + self.controller.guided( + GuidedChoiceV2(target=target, capability=GuidedCapability.DIRECT)) parts_after = gaps.parts_and_gaps(self.d1)[:-1] self.assertEqual(parts_before, parts_after) p = gaps.parts_and_gaps(self.d1)[-1] @@ -202,8 +220,10 @@ class TestGuided(IsolatedAsyncioTestCase): self._guided_side_by_side(bl, pt) parts_before = self.d1._partitions.copy() gap = gaps.largest_gap(self.d1) - target = GuidedStorageTargetUseGap(disk_id=self.d1.id, gap=gap) - self.controller.guided(GuidedChoiceV2(target=target, use_lvm=True)) + target = GuidedStorageTargetUseGap( + disk_id=self.d1.id, gap=gap, capabilities=default_capabilities) + self.controller.guided( + GuidedChoiceV2(target=target, capability=GuidedCapability.LVM)) parts_after = gaps.parts_and_gaps(self.d1)[:-2] self.assertEqual(parts_before, parts_after) p_boot, p_data = gaps.parts_and_gaps(self.d1)[-2:] @@ -254,7 +274,10 @@ class TestGuidedV2(IsolatedAsyncioTestCase): async def test_blank_disk(self, bootloader, ptable): # blank disks should not report a UseGap case self._setup(bootloader, ptable, fix_bios=False) - expected = [GuidedStorageTargetReformat(disk_id=self.disk.id)] + expected = [ + GuidedStorageTargetReformat( + disk_id=self.disk.id, capabilities=default_capabilities), + ] resp = await self.fsc.v2_guided_GET() self.assertEqual(expected, resp.possible) self.assertEqual(ProbeStatus.DONE, resp.status) @@ -282,8 +305,10 @@ class TestGuidedV2(IsolatedAsyncioTestCase): resp = await self.fsc.v2_guided_GET() reformat = resp.possible.pop(0) - self.assertEqual(GuidedStorageTargetReformat(disk_id=self.disk.id), - reformat) + self.assertEqual( + GuidedStorageTargetReformat( + disk_id=self.disk.id, capabilities=default_capabilities), + reformat) use_gap = resp.possible.pop(0) self.assertEqual(self.disk.id, use_gap.disk_id) @@ -303,8 +328,10 @@ class TestGuidedV2(IsolatedAsyncioTestCase): self.fs_probe[p._path()] = {'ESTIMATED_MIN_SIZE': 1 << 20} resp = await self.fsc.v2_guided_GET() reformat = resp.possible.pop(0) - self.assertEqual(GuidedStorageTargetReformat(disk_id=self.disk.id), - reformat) + self.assertEqual( + GuidedStorageTargetReformat( + disk_id=self.disk.id, capabilities=default_capabilities), + reformat) resize = resp.possible.pop(0) self.assertEqual(self.disk.id, resize.disk_id) @@ -323,17 +350,20 @@ class TestGuidedV2(IsolatedAsyncioTestCase): self.fsc.calculate_suggested_install_min.return_value = 10 << 30 resp = await self.fsc.v2_guided_GET() reformat = resp.possible.pop(0) - self.assertEqual(GuidedStorageTargetReformat(disk_id=self.disk.id), - reformat) + self.assertEqual( + GuidedStorageTargetReformat( + disk_id=self.disk.id, capabilities=default_capabilities), + reformat) resize = resp.possible.pop(0) expected = GuidedStorageTargetResize( - disk_id=self.disk.id, - partition_number=p.number, - new_size=200 << 30, - minimum=50 << 30, - recommended=200 << 30, - maximum=230 << 30) + disk_id=self.disk.id, + partition_number=p.number, + new_size=200 << 30, + minimum=50 << 30, + recommended=200 << 30, + maximum=230 << 30, + capabilities=default_capabilities) self.assertEqual(expected, resize) self.assertEqual(0, len(resp.possible)) @@ -353,7 +383,8 @@ class TestGuidedV2(IsolatedAsyncioTestCase): resize = guided_get_resp.possible.pop(0) self.assertTrue(isinstance(resize, GuidedStorageTargetResize)) - data = GuidedChoiceV2(target=reformat) + data = GuidedChoiceV2( + target=reformat, capability=GuidedCapability.DIRECT) expected_config = copy.copy(data) resp = await self.fsc.v2_guided_POST(data=data) self.assertEqual(expected_config, resp.configured) @@ -379,7 +410,8 @@ class TestGuidedV2(IsolatedAsyncioTestCase): use_gap = guided_get_resp.possible.pop(0) self.assertTrue(isinstance(use_gap, GuidedStorageTargetUseGap)) self.assertEqual(g, use_gap.gap) - data = GuidedChoiceV2(target=use_gap) + data = GuidedChoiceV2( + target=use_gap, capability=GuidedCapability.DIRECT) expected_config = copy.copy(data) resp = await self.fsc.v2_guided_POST(data=data) self.assertEqual(expected_config, resp.configured) @@ -416,7 +448,8 @@ class TestGuidedV2(IsolatedAsyncioTestCase): p_expected = copy.copy(orig_p) p_expected.size = resize.new_size = 20 << 30 p_expected.resize = True - data = GuidedChoiceV2(target=resize) + data = GuidedChoiceV2( + target=resize, capability=GuidedCapability.DIRECT) expected_config = copy.copy(data) resp = await self.fsc.v2_guided_POST(data=data) self.assertEqual(expected_config, resp.configured) @@ -442,7 +475,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase): reformat = guided_get_resp.possible.pop(0) self.assertTrue(isinstance(reformat, GuidedStorageTargetReformat)) - data = GuidedChoiceV2(target=reformat, use_lvm=True) + data = GuidedChoiceV2(target=reformat, capability=GuidedCapability.LVM) expected_config = copy.copy(data) resp = await self.fsc.v2_guided_POST(data=data) diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index a71bb3ed..e2196230 100644 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -330,7 +330,12 @@ class TestFlow(TestAPI): resp = await inst.get('/storage/v2/guided?wait=true') [reformat] = resp['possible'] - await inst.post('/storage/v2/guided', {'target': reformat}) + await inst.post( + '/storage/v2/guided', + { + 'target': reformat, + 'capability': reformat['capabilities'][0], + }) await inst.post('/storage/v2') await inst.get('/meta/status', cur='WAITING') await inst.post('/meta/confirm', tty='/dev/tty1') @@ -418,7 +423,11 @@ class TestFlow(TestAPI): resp = await inst.get('/storage/v2/guided') [reformat] = match(resp['possible'], _type='GuidedStorageTargetReformat') - await inst.post('/storage/v2/guided', {'target': reformat}) + data = { + 'target': reformat, + 'capability': reformat['capabilities'][0], + } + await inst.post('/storage/v2/guided', data) after_guided_resp = await inst.get('/storage/v2') post_resp = await inst.post('/storage/v2') # posting to the endpoint shouldn't change the answer @@ -433,7 +442,12 @@ class TestGuided(TestAPI): resp = await inst.get('/storage/v2/guided') [reformat] = match(resp['possible'], _type='GuidedStorageTargetReformat') - resp = await inst.post('/storage/v2/guided', {'target': reformat}) + resp = await inst.post( + '/storage/v2/guided', + { + 'target': reformat, + 'capability': reformat['capabilities'][0], + }) self.assertEqual(reformat, resp['configured']['target']) resp = await inst.get('/storage/v2') [p1, p2] = resp['disks'][0]['partitions'] @@ -475,7 +489,10 @@ class TestGuided(TestAPI): [resize_ntfs, resize_ext4] = match( resp['possible'], _type='GuidedStorageTargetResize') resize_ntfs['new_size'] = 30 << 30 - data = {'target': resize_ntfs} + data = { + 'target': resize_ntfs, + 'capability': resize_ntfs['capabilities'][0], + } resp = await inst.post('/storage/v2/guided', data) self.assertEqual(resize_ntfs, resp['configured']['target']) resp = await inst.get('/storage/v2') @@ -519,7 +536,10 @@ class TestGuided(TestAPI): resp = await inst.get('/storage/v2/guided') [use_gap] = match(resp['possible'], _type='GuidedStorageTargetUseGap') - data = {'target': use_gap} + data = { + 'target': use_gap, + 'capability': use_gap['capabilities'][0], + } resp = await inst.post('/storage/v2/guided', data) self.assertEqual(use_gap, resp['configured']['target']) resp = await inst.get('/storage/v2') @@ -555,7 +575,10 @@ class TestGuided(TestAPI): [resize] = match( resp['possible'], _type='GuidedStorageTargetResize', partition_number=6) - data = {'target': resize} + data = { + 'target': resize, + 'capability': resize['capabilities'][0], + } resp = await inst.post('/storage/v2/guided', data) self.assertEqual(resize, resp['configured']['target']) # should not throw a Gap Not Found exception @@ -980,7 +1003,11 @@ class TestTodos(TestAPI): # server indicators of required client actions resp = await inst.get('/storage/v2/guided') [reformat] = resp['possible'] - await inst.post('/storage/v2/guided', {'target': reformat}) + data = { + 'target': reformat, + 'capability': reformat['capabilities'][0], + } + await inst.post('/storage/v2/guided', data) resp = await inst.get('/storage/v2') self.assertFalse(resp['need_root']) self.assertFalse(resp['need_boot']) @@ -1181,7 +1208,11 @@ class TestPartitionTableEditing(TestAPI): resize = match(resp['possible'], _type='GuidedStorageTargetResize')[0] resize['new_size'] = 30 << 30 - await inst.post('/storage/v2/guided', {'target': resize}) + data = { + 'target': resize, + 'capability': resize['capabilities'][0], + } + await inst.post('/storage/v2/guided', data) orig_config = await inst.get('/storage/v2/orig_config') end_resp = await inst.get('/storage/v2') self.assertEqual(start_resp, orig_config)