Merge pull request #1706 from mwhudson/manual-capability

add a target/capability for manual partitioning
This commit is contained in:
Michael Hudson-Doyle 2023-07-08 21:31:03 +12:00 committed by GitHub
commit 95e520ddf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 42 deletions

View File

@ -29,6 +29,7 @@ from subiquity.common.types import (
GuidedCapability, GuidedCapability,
GuidedChoiceV2, GuidedChoiceV2,
GuidedStorageResponseV2, GuidedStorageResponseV2,
GuidedStorageTargetManual,
GuidedStorageTargetReformat, GuidedStorageTargetReformat,
StorageResponseV2, StorageResponseV2,
) )
@ -177,7 +178,10 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
self.finish() self.finish()
elif self.answers['manual']: elif self.answers['manual']:
await self._guided_choice(None) await self._guided_choice(
GuidedChoiceV2(
target=GuidedStorageTargetManual(),
capability=GuidedCapability.MANUAL))
await self._run_actions(self.answers['manual']) await self._run_actions(self.answers['manual'])
self.answers['manual'] = [] self.answers['manual'] = []
@ -287,21 +291,11 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
else: else:
raise Exception("could not process action {}".format(action)) raise Exception("could not process action {}".format(action))
async def _guided_choice(self, choice: Optional[GuidedChoiceV2]): async def _guided_choice(self, choice: GuidedChoiceV2):
if self.core_boot_capability is not None: coro = self.endpoint.guided.POST(choice)
self.app.next_screen(self.endpoint.guided.POST(choice)) if not choice.capability.supports_manual_customization():
self.app.next_screen(coro)
return return
# FIXME It would seem natural here to pass the wait=true flag to the
# below HTTP calls, especially because we wrap the coroutine in
# wait_with_progress.
# Having said that, making the server return a cached result seems like
# the least risky option to address https://launchpad.net/bugs/1993257
# before the kinetic release. This is also similar to what we did for
# https://launchpad.net/bugs/1962205
if choice is not None:
coro = self.endpoint.guided.POST(choice)
else:
coro = self.endpoint.GET(use_cached_result=True)
status = await self.app.wait_with_progress(coro) status = await self.app.wait_with_progress(coro)
self.model = FilesystemModel(status.bootloader) self.model = FilesystemModel(status.bootloader)
self.model.load_server_data(status) self.model.load_server_data(status)

View File

@ -321,6 +321,7 @@ class Disk:
class GuidedCapability(enum.Enum): class GuidedCapability(enum.Enum):
MANUAL = enum.auto()
DIRECT = enum.auto() DIRECT = enum.auto()
LVM = enum.auto() LVM = enum.auto()
LVM_LUKS = enum.auto() LVM_LUKS = enum.auto()
@ -340,6 +341,14 @@ class GuidedCapability(enum.Enum):
GuidedCapability.CORE_BOOT_PREFER_ENCRYPTED, GuidedCapability.CORE_BOOT_PREFER_ENCRYPTED,
GuidedCapability.CORE_BOOT_PREFER_UNENCRYPTED] GuidedCapability.CORE_BOOT_PREFER_UNENCRYPTED]
def supports_manual_customization(self) -> bool:
# After posting this capability to guided_POST, is it possible
# for the user to customize the layout further?
return self in [GuidedCapability.MANUAL,
GuidedCapability.DIRECT,
GuidedCapability.LVM,
GuidedCapability.LVM_LUKS]
class GuidedDisallowedCapabilityReason(enum.Enum): class GuidedDisallowedCapabilityReason(enum.Enum):
TOO_SMALL = enum.auto() TOO_SMALL = enum.auto()
@ -437,9 +446,17 @@ class GuidedStorageTargetUseGap:
disallowed: List[GuidedDisallowedCapability] = attr.Factory(list) disallowed: List[GuidedDisallowedCapability] = attr.Factory(list)
@attr.s(auto_attribs=True)
class GuidedStorageTargetManual:
allowed: List[GuidedCapability] = attr.Factory(
lambda: [GuidedCapability.MANUAL])
disallowed: List[GuidedDisallowedCapability] = attr.Factory(list)
GuidedStorageTarget = Union[GuidedStorageTargetReformat, GuidedStorageTarget = Union[GuidedStorageTargetReformat,
GuidedStorageTargetResize, GuidedStorageTargetResize,
GuidedStorageTargetUseGap] GuidedStorageTargetUseGap,
GuidedStorageTargetManual]
@attr.s(auto_attribs=True) @attr.s(auto_attribs=True)
@ -447,8 +464,7 @@ class GuidedChoiceV2:
target: GuidedStorageTarget target: GuidedStorageTarget
capability: GuidedCapability capability: GuidedCapability
password: Optional[str] = attr.ib(default=None, repr=False) password: Optional[str] = attr.ib(default=None, repr=False)
sizing_policy: Optional[SizingPolicy] = \ sizing_policy: Optional[SizingPolicy] = SizingPolicy.SCALED
attr.ib(default=SizingPolicy.SCALED)
reset_partition: bool = False reset_partition: bool = False

View File

@ -67,6 +67,7 @@ from subiquity.common.types import (
GuidedDisallowedCapabilityReason, GuidedDisallowedCapabilityReason,
GuidedStorageResponseV2, GuidedStorageResponseV2,
GuidedStorageTarget, GuidedStorageTarget,
GuidedStorageTargetManual,
GuidedStorageTargetReformat, GuidedStorageTargetReformat,
GuidedStorageTargetResize, GuidedStorageTargetResize,
GuidedStorageTargetUseGap, GuidedStorageTargetUseGap,
@ -544,6 +545,9 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
choice: GuidedChoiceV2, choice: GuidedChoiceV2,
reset_partition_only: bool = False reset_partition_only: bool = False
) -> None: ) -> None:
if choice.capability == GuidedCapability.MANUAL:
return
self.model.guided_configuration = choice self.model.guided_configuration = choice
self.set_info_for_capability(choice.capability) self.set_info_for_capability(choice.capability)
@ -784,7 +788,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
async def guided_POST(self, data: GuidedChoiceV2) -> StorageResponse: async def guided_POST(self, data: GuidedChoiceV2) -> StorageResponse:
log.debug(data) log.debug(data)
await self.guided(data) await self.guided(data)
if data.capability.is_core_boot(): if not data.capability.supports_manual_customization():
await self.configured() await self.configured()
return self._done_response() return self._done_response()
@ -886,18 +890,14 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
async def v2_ensure_transaction_POST(self) -> None: async def v2_ensure_transaction_POST(self) -> None:
self.locked_probe_data = True self.locked_probe_data = True
def get_available_capabilities(self): def get_classic_capabilities(self):
classic_capabilities = set() classic_capabilities = set()
core_boot_capabilities = set()
for info in self._variation_info.values(): for info in self._variation_info.values():
if not info.is_valid(): if not info.is_valid():
continue continue
if info.is_core_boot_classic(): if not info.is_core_boot_classic():
core_boot_capabilities.update(info.capability_info.allowed)
else:
classic_capabilities.update(info.capability_info.allowed) classic_capabilities.update(info.capability_info.allowed)
return sorted(classic_capabilities, key=lambda x: x.name), \ return sorted(classic_capabilities, key=lambda x: x.name)
sorted(core_boot_capabilities, key=lambda x: x.name)
async def v2_guided_GET(self, wait: bool = False) \ async def v2_guided_GET(self, wait: bool = False) \
-> GuidedStorageResponseV2: -> GuidedStorageResponseV2:
@ -911,8 +911,10 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
scenarios = [] scenarios = []
install_min = self.calculate_suggested_install_min() install_min = self.calculate_suggested_install_min()
classic_capabilities, core_boot_capabilities = \ classic_capabilities = self.get_classic_capabilities()
self.get_available_capabilities()
if GuidedCapability.DIRECT in classic_capabilities:
scenarios.append((0, GuidedStorageTargetManual()))
for disk in self.potential_boot_disks(with_reformatting=True): for disk in self.potential_boot_disks(with_reformatting=True):
capability_info = CapabilityInfo() capability_info = CapabilityInfo()

View File

@ -36,6 +36,7 @@ from subiquity.common.types import (
GuidedCapability, GuidedCapability,
GuidedDisallowedCapabilityReason, GuidedDisallowedCapabilityReason,
GuidedChoiceV2, GuidedChoiceV2,
GuidedStorageTargetManual,
GuidedStorageTargetReformat, GuidedStorageTargetReformat,
GuidedStorageTargetResize, GuidedStorageTargetResize,
GuidedStorageTargetUseGap, GuidedStorageTargetUseGap,
@ -540,6 +541,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
expected = [ expected = [
GuidedStorageTargetReformat( GuidedStorageTargetReformat(
disk_id=self.disk.id, allowed=default_capabilities), disk_id=self.disk.id, allowed=default_capabilities),
GuidedStorageTargetManual(),
] ]
resp = await self.fsc.v2_guided_GET() resp = await self.fsc.v2_guided_GET()
self.assertEqual(expected, resp.targets) self.assertEqual(expected, resp.targets)
@ -553,11 +555,23 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.assertEqual([], resp.targets) self.assertEqual([], resp.targets)
self.assertEqual(ProbeStatus.PROBING, resp.status) self.assertEqual(ProbeStatus.PROBING, resp.status)
async def test_manual(self):
await self._setup(Bootloader.UEFI, 'gpt')
guided_get_resp = await self.fsc.v2_guided_GET()
[reformat, manual] = guided_get_resp.targets
self.assertEqual(manual, GuidedStorageTargetManual())
data = GuidedChoiceV2(target=reformat, capability=manual.allowed[0])
# POSTing the manual choice doesn't change anything
await self.fsc.v2_guided_POST(data=data)
guided_get_resp = await self.fsc.v2_guided_GET()
self.assertEqual([reformat, manual], guided_get_resp.targets)
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_small_blank_disk(self, bootloader, ptable): async def test_small_blank_disk(self, bootloader, ptable):
await self._setup(bootloader, ptable, size=1 << 30) await self._setup(bootloader, ptable, size=1 << 30)
resp = await self.fsc.v2_guided_GET() resp = await self.fsc.v2_guided_GET()
self.assertEqual(1, len(resp.targets)) self.assertEqual(2, len(resp.targets))
self.assertEqual(GuidedStorageTargetManual(), resp.targets[1])
self.assertEqual(0, len(resp.targets[0].allowed)) self.assertEqual(0, len(resp.targets[0].allowed))
self.assertEqual( self.assertEqual(
{ {
@ -600,7 +614,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.assertEqual(self.disk.id, resize.disk_id) self.assertEqual(self.disk.id, resize.disk_id)
self.assertEqual(p.number, resize.partition_number) self.assertEqual(p.number, resize.partition_number)
self.assertTrue(isinstance(resize, GuidedStorageTargetResize)) self.assertTrue(isinstance(resize, GuidedStorageTargetResize))
self.assertEqual(0, len(resp.targets)) self.assertEqual(1, len(resp.targets))
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_used_full_disk(self, bootloader, ptable): async def test_used_full_disk(self, bootloader, ptable):
@ -619,7 +633,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.assertEqual(self.disk.id, resize.disk_id) self.assertEqual(self.disk.id, resize.disk_id)
self.assertEqual(p.number, resize.partition_number) self.assertEqual(p.number, resize.partition_number)
self.assertTrue(isinstance(resize, GuidedStorageTargetResize)) self.assertTrue(isinstance(resize, GuidedStorageTargetResize))
self.assertEqual(0, len(resp.targets)) self.assertEqual(1, len(resp.targets))
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_weighted_split(self, bootloader, ptable): async def test_weighted_split(self, bootloader, ptable):
@ -648,7 +662,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
maximum=230 << 30, maximum=230 << 30,
allowed=default_capabilities) allowed=default_capabilities)
self.assertEqual(expected, resize) self.assertEqual(expected, resize)
self.assertEqual(0, len(possible)) self.assertEqual(1, len(possible))
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_half_disk_reformat(self, bootloader, ptable): async def test_half_disk_reformat(self, bootloader, ptable):
@ -675,7 +689,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
resp = await self.fsc.v2_GET() resp = await self.fsc.v2_GET()
self.assertFalse(resp.need_root) self.assertFalse(resp.need_root)
self.assertFalse(resp.need_boot) self.assertFalse(resp.need_boot)
self.assertEqual(0, len(guided_get_resp.targets)) self.assertEqual(1, len(guided_get_resp.targets))
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_half_disk_use_gap(self, bootloader, ptable): async def test_half_disk_use_gap(self, bootloader, ptable):
@ -708,7 +722,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.assertEqual(orig_p, existing_part) self.assertEqual(orig_p, existing_part)
self.assertFalse(resp.need_root) self.assertFalse(resp.need_root)
self.assertFalse(resp.need_boot) self.assertFalse(resp.need_boot)
self.assertEqual(0, len(guided_get_resp.targets)) self.assertEqual(1, len(guided_get_resp.targets))
@parameterized.expand(bootloaders_and_ptables) @parameterized.expand(bootloaders_and_ptables)
async def test_half_disk_resize(self, bootloader, ptable): async def test_half_disk_resize(self, bootloader, ptable):
@ -743,7 +757,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.assertEqual(p_expected, existing_part) self.assertEqual(p_expected, existing_part)
self.assertFalse(resp.need_root) self.assertFalse(resp.need_root)
self.assertFalse(resp.need_boot) self.assertFalse(resp.need_boot)
self.assertEqual(0, len(guided_get_resp.targets)) self.assertEqual(1, len(guided_get_resp.targets))
@parameterized.expand([ @parameterized.expand([
[10], [20], [25], [30], [50], [100], [250], [10], [20], [25], [30], [50], [100], [250],

View File

@ -332,7 +332,7 @@ class TestFlow(TestAPI):
{'elected': 'http://us.archive.ubuntu.com/ubuntu'}) {'elected': 'http://us.archive.ubuntu.com/ubuntu'})
resp = await inst.get('/storage/v2/guided?wait=true') resp = await inst.get('/storage/v2/guided?wait=true')
[reformat] = resp['targets'] [reformat, manual] = resp['targets']
await inst.post( await inst.post(
'/storage/v2/guided', '/storage/v2/guided',
{ {
@ -605,7 +605,7 @@ class TestCore(TestAPI):
async with start_server(cfg, **kw) as inst: async with start_server(cfg, **kw) as inst:
await inst.post('/source', source_id='ubuntu-desktop') await inst.post('/source', source_id='ubuntu-desktop')
resp = await inst.get('/storage/v2/guided', wait=True) resp = await inst.get('/storage/v2/guided', wait=True)
[reformat] = resp['targets'] [reformat, manual] = resp['targets']
self.assertIn('CORE_BOOT_PREFER_ENCRYPTED', self.assertIn('CORE_BOOT_PREFER_ENCRYPTED',
reformat['allowed']) reformat['allowed'])
data = dict(target=reformat, capability='CORE_BOOT_ENCRYPTED') data = dict(target=reformat, capability='CORE_BOOT_ENCRYPTED')
@ -1042,7 +1042,7 @@ class TestTodos(TestAPI): # server indicators of required client actions
self.assertTrue(resp['need_boot']) self.assertTrue(resp['need_boot'])
resp = await inst.get('/storage/v2/guided') resp = await inst.get('/storage/v2/guided')
[reformat] = resp['targets'] [reformat, manual] = resp['targets']
data = { data = {
'target': reformat, 'target': reformat,
'capability': reformat['allowed'][0], 'capability': reformat['allowed'][0],

View File

@ -50,6 +50,7 @@ from subiquity.common.types import (
Gap, Gap,
GuidedCapability, GuidedCapability,
GuidedChoiceV2, GuidedChoiceV2,
GuidedStorageTargetManual,
GuidedStorageTargetReformat, GuidedStorageTargetReformat,
Partition, Partition,
) )
@ -313,12 +314,11 @@ class GuidedDiskSelectionView(BaseView):
def done(self, sender): def done(self, sender):
results = sender.as_data() results = sender.as_data()
password = None password = None
capability = None
disk_id = None disk_id = None
if self.controller.core_boot_capability is not None: if self.controller.core_boot_capability is not None:
if results.get('use_tpm', sender.tpm_choice.default): if results.get('use_tpm', sender.tpm_choice.default):
capability = GuidedCapability.CORE_BOOT_ENCRYPTED capability = GuidedCapability.CORE_BOOT_ENCRYPTED
else:
capability = GuidedCapability.CORE_BOOT_UNENCRYPTED
disk_id = results['disk'].id disk_id = results['disk'].id
elif results['guided']: elif results['guided']:
if results['guided_choice']['use_lvm']: if results['guided_choice']['use_lvm']:
@ -341,11 +341,17 @@ class GuidedDiskSelectionView(BaseView):
password=password, password=password,
) )
else: else:
choice = None choice = GuidedChoiceV2(
target=GuidedStorageTargetManual(),
capability=GuidedCapability.MANUAL,
)
self.controller.guided_choice(choice) self.controller.guided_choice(choice)
def manual(self, sender): def manual(self, sender):
self.controller.guided_choice(None) self.controller.guided_choice(GuidedChoiceV2(
target=GuidedStorageTargetManual(),
capability=GuidedCapability.MANUAL,
))
def cancel(self, btn=None): def cancel(self, btn=None):
self.controller.cancel() self.controller.cancel()