From ba3dbb52ef3b0e46ddbc7f6c657db50d0b50a921 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Fri, 22 Mar 2024 16:49:19 -0700 Subject: [PATCH] autoinstall: Allow "autoinstall" as top-level key --- subiquity/server/server.py | 23 +++++++++++-- subiquity/server/tests/test_server.py | 49 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 61f2aa39..77e943b7 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -673,9 +673,28 @@ class SubiquityServer(Application): with open(cfg_path) as fp: config: dict[str, Any] = yaml.safe_load(fp) - autoinstall_config: dict[str, Any] = dict() + autoinstall_config: dict[str, Any] - autoinstall_config = config + # Support "autoinstall" as a top-level key + if "autoinstall" in config: + autoinstall_config = config.pop("autoinstall") + + # but the only top level key + if len(config) != 0: + self.interactive = bool(autoinstall_config.get("interactive-sections")) + msg: str = ( + "autoinstall.yaml is not a valid cloud config datasource.\n" + "No other keys may be present alongside 'autoinstall' at " + "the top level." + ) + context.error(msg) + raise AutoinstallValidationError( + owner="top-level keys", + details="autoinstall.yaml is not a valid cloud config datasource", + ) + + else: + autoinstall_config = config return autoinstall_config diff --git a/subiquity/server/tests/test_server.py b/subiquity/server/tests/test_server.py index e776fae9..a9dced2a 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -20,6 +20,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import jsonschema +import yaml from jsonschema.validators import validator_for from subiquity.cloudinit import CloudInitSchemaValidationError @@ -156,6 +157,7 @@ early-commands: ["{cmd}"] class TestAutoinstallValidation(SubiTestCase): async def asyncSetUp(self): + self.tempdir = self.tmp_dir() opts = Mock() opts.dry_run = True opts.output_base = self.tmp_dir() @@ -171,6 +173,15 @@ class TestAutoinstallValidation(SubiTestCase): } self.server.make_apport_report = Mock() + def path(self, relative_path): + return self.tmp_path(relative_path, dir=self.tempdir) + + def create(self, path, contents): + path = self.path(path) + with open(path, "w") as fp: + fp.write(contents) + return path + # Pseudo Load Controllers to avoid patching the loading logic for each # controller when we still want access to class attributes def pseudo_load_controllers(self): @@ -445,6 +456,44 @@ class TestAutoinstallValidation(SubiTestCase): self.assertEqual(cfg, expected) + async def test_autoinstall_validation__top_level_autoinstall(self): + """Test allow autoinstall as top-level key""" + + new_style = { + "autoinstall": { + "version": 1, + "interactive-sections": ["identity"], + "apt": "...", + } + } + old_style = new_style["autoinstall"] + + # Read new style correctly + path = self.create("autoinstall.yaml", yaml.dump(new_style)) + self.assertEqual(self.server._read_config(cfg_path=path), old_style) + + # No changes to old style + path = self.create("autoinstall.yaml", yaml.dump(old_style)) + self.assertEqual(self.server._read_config(cfg_path=path), old_style) + + async def test_autoinstall_validation__not_cloudinit_datasource(self): + """Test no cloud init datasources in new style autoinstall""" + + new_style = { + "autoinstall": { + "version": 1, + "interactive-sections": ["identity"], + "apt": "...", + }, + "cloudinit-data": "I am data", + } + + with self.assertRaises(AutoinstallValidationError) as ctx: + path = self.create("autoinstall.yaml", yaml.dump(new_style)) + self.server._read_config(cfg_path=path) + + self.assertEqual("top-level keys", ctx.exception.owner) + class TestMetaController(SubiTestCase): async def test_interactive_sections_not_present(self):