From c7fd905c6b03b596391dd0b80ea6cb76a48a1b0d Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Mon, 6 May 2024 14:41:38 +0200 Subject: [PATCH] allow system definition to be in live layer Rather than always assuming it has to be mounted in from the layer to be installed (snapd will now populate the seed in the target system when its install API is called if it is empty). --- examples/snaps/v2-systems.json | 43 +++++++++++++++++++ subiquity/server/controllers/filesystem.py | 36 ++++++++++------ .../controllers/tests/test_filesystem.py | 15 ++++++- subiquity/server/snapdapi.py | 9 ++++ 4 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 examples/snaps/v2-systems.json diff --git a/examples/snaps/v2-systems.json b/examples/snaps/v2-systems.json new file mode 100644 index 00000000..666861a5 --- /dev/null +++ b/examples/snaps/v2-systems.json @@ -0,0 +1,43 @@ +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "systems": [ + { + "current": true, + "label": "20240314", + "model": { + "model": "ubuntu-core-24-amd64-dangerous", + "brand-id": "canonical", + "display-name": "ubuntu-core-24-amd64-dangerous" + }, + "brand": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "actions": [ + { + "title": "Reinstall", + "mode": "install" + }, + { + "title": "Recover", + "mode": "recover" + }, + { + "title": "Factory reset", + "mode": "factory-reset" + }, + { + "title": "Run normally", + "mode": "run" + } + ], + "default-recovery-system": true + } + ] + } +} diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index 1a58c0c0..84192cad 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -337,17 +337,26 @@ class FilesystemController(SubiquityController, FilesystemManipulator): self._source_handler = None async def _get_system(self, variation_name, label): - try: - await self._mount_systems_dir(variation_name) - except NoSnapdSystemsOnSource: - return None - try: - system = await self.app.snapdapi.v2.systems[label].GET() - except requests.exceptions.HTTPError as http_err: - log.warning("v2/systems/%s returned %s", label, http_err.response.text) - raise - finally: - await self._unmount_systems_dir() + systems = await self.app.snapdapi.v2.systems.GET() + labels = {system.label for system in systems.systems} + if label in labels: + try: + system = await self.app.snapdapi.v2.systems[label].GET() + except requests.exceptions.HTTPError as http_err: + log.warning("v2/systems/%s returned %s", label, http_err.response.text) + raise + else: + try: + await self._mount_systems_dir(variation_name) + except NoSnapdSystemsOnSource: + return None + try: + system = await self.app.snapdapi.v2.systems[label].GET() + except requests.exceptions.HTTPError as http_err: + log.warning("v2/systems/%s returned %s", label, http_err.response.text) + raise + finally: + await self._unmount_systems_dir() log.debug("got system %s", system) return system @@ -858,7 +867,10 @@ class FilesystemController(SubiquityController, FilesystemManipulator): async def guided_core_boot(self, disk: Disk): # Formatting for a core boot classic system relies on some curtin # features that are only available with v2 partitioning. - await self._mount_systems_dir(self._info.name) + systems = await self.app.snapdapi.v2.systems.GET() + labels = {system.label for system in systems.systems} + if self._info.label not in labels: + await self._mount_systems_dir(self._info.name) self.model.storage_version = 2 [volume] = self._info.system.volumes.values() self._on_volume = snapdapi.OnVolume.from_volume(volume) diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py index bbad135f..0cc4ede5 100644 --- a/subiquity/server/controllers/tests/test_filesystem.py +++ b/subiquity/server/controllers/tests/test_filesystem.py @@ -432,6 +432,18 @@ class TestSubiquityControllerFilesystem(IsolatedAsyncioTestCase): }, } requests_mocker = requests_mock.Mocker() + requests_mocker.get( + "http+unix://snapd/v2/systems", + json={ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "systems": [], + }, + }, + status_code=200, + ) requests_mocker.get( "http+unix://snapd/v2/systems/enhanced-secureboot-desktop", json=json_body, @@ -1397,9 +1409,10 @@ class TestCoreBootInstallMethods(IsolatedAsyncioTestCase): name="foo", label="system", system=snapdapi.SystemDetails( + label="system", volumes={ "pc": snapdapi.Volume(schema="gpt", structure=structures), - } + }, ), ) diff --git a/subiquity/server/snapdapi.py b/subiquity/server/snapdapi.py index 5ae38382..2231cb61 100644 --- a/subiquity/server/snapdapi.py +++ b/subiquity/server/snapdapi.py @@ -203,6 +203,7 @@ class StorageEncryption: @attr.s(auto_attribs=True) class SystemDetails: + label: str current: bool = False volumes: Dict[str, Volume] = attr.Factory(dict) storage_encryption: Optional[StorageEncryption] = named_field( @@ -210,6 +211,11 @@ class SystemDetails: ) +@attr.s(auto_attribs=True) +class SystemsResponse: + systems: List[SystemDetails] = attr.Factory(list) + + class SystemAction(enum.Enum): INSTALL = "install" @@ -258,6 +264,9 @@ class SnapdAPI: ... class systems: + def GET() -> SystemsResponse: + ... + @path_parameter class label: def GET() -> SystemDetails: