diff --git a/autoinstall-schema.json b/autoinstall-schema.json
index 5684adbc6..e16195ac3 100644
--- a/autoinstall-schema.json
+++ b/autoinstall-schema.json
@@ -6,6 +6,12 @@
"minimum": 1,
"maximum": 1
},
+ "interactive-sections": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"early-commands": {
"type": "array",
"items": {
diff --git a/autoinstall-system-setup-schema.json b/autoinstall-system-setup-schema.json
index 41cc97ac7..8fd1d6732 100644
--- a/autoinstall-system-setup-schema.json
+++ b/autoinstall-system-setup-schema.json
@@ -6,6 +6,12 @@
"minimum": 1,
"maximum": 1
},
+ "interactive-sections": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"early-commands": {
"type": "array",
"items": {
diff --git a/subiquity/server/autoinstall.py b/subiquity/server/autoinstall.py
new file mode 100644
index 000000000..0abc3dd37
--- /dev/null
+++ b/subiquity/server/autoinstall.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+import logging
+
+log = logging.getLogger("subiquity.server.autoinstall")
+
+
+class AutoinstallError(Exception):
+ pass
+
+
+class AutoinstallValidationError(AutoinstallError):
+ def __init__(
+ self,
+ owner: str,
+ ):
+ self.message = f"Malformed autoinstall in {owner!r} section"
+ self.owner = owner
+ super().__init__(self.message)
diff --git a/subiquity/server/controller.py b/subiquity/server/controller.py
index 4662f1877..6a5d64d7e 100644
--- a/subiquity/server/controller.py
+++ b/subiquity/server/controller.py
@@ -19,8 +19,10 @@
from typing import Any, Optional
import jsonschema
+from jsonschema.exceptions import ValidationError
from subiquity.common.api.server import bind
+from subiquity.server.autoinstall import AutoinstallValidationError
from subiquity.server.types import InstallerChannels
from subiquitycore.context import with_context
from subiquitycore.controller import BaseController
@@ -54,6 +56,19 @@ async def _confirmed(self):
await self.configured()
self._active = False
+ def validate_autoinstall(self, ai_data: dict) -> None:
+ try:
+ jsonschema.validate(ai_data, self.autoinstall_schema)
+
+ except ValidationError as original_exception:
+ section = self.autoinstall_key
+
+ new_exception: AutoinstallValidationError = AutoinstallValidationError(
+ section,
+ )
+
+ raise new_exception from original_exception
+
def setup_autoinstall(self):
if not self.app.autoinstall_config:
return
@@ -72,7 +87,8 @@ def setup_autoinstall(self):
ai_data = self.autoinstall_default
if ai_data is not None and self.autoinstall_schema is not None:
- jsonschema.validate(ai_data, self.autoinstall_schema)
+ self.validate_autoinstall(ai_data)
+
self.load_autoinstall_data(ai_data)
def load_autoinstall_data(self, data):
diff --git a/subiquity/server/controllers/tests/test_cmdlist.py b/subiquity/server/controllers/tests/test_cmdlist.py
new file mode 100644
index 000000000..5878a1115
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_cmdlist.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.cmdlist import CmdListController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestCmdListController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ CmdListController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(CmdListController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_codecs.py b/subiquity/server/controllers/tests/test_codecs.py
new file mode 100644
index 000000000..ad3ef9c9a
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_codecs.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.codecs import CodecsController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestCodecsController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ CodecsController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(CodecsController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_debconf.py b/subiquity/server/controllers/tests/test_debconf.py
new file mode 100644
index 000000000..4e1765950
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_debconf.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.debconf import DebconfController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestDebconfController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ DebconfController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(DebconfController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_drivers.py b/subiquity/server/controllers/tests/test_drivers.py
new file mode 100644
index 000000000..41482c783
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_drivers.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.drivers import DriversController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestDriversController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ DriversController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(DriversController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py
index 4321601df..8e1bd6830 100644
--- a/subiquity/server/controllers/tests/test_filesystem.py
+++ b/subiquity/server/controllers/tests/test_filesystem.py
@@ -18,7 +18,9 @@
import uuid
from unittest import IsolatedAsyncioTestCase, mock
+import jsonschema
from curtin.commands.extract import TrivialSourceHandler
+from jsonschema.validators import validator_for
from subiquity.common.filesystem import gaps, labels
from subiquity.common.filesystem.actions import DeviceAction
@@ -399,6 +401,15 @@ async def test_examine_systems(self):
self.assertEqual(len(self.fsc._variation_info), 1)
self.assertEqual(self.fsc._variation_info["default"].name, "default")
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ FilesystemController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(FilesystemController.autoinstall_schema)
+
class TestGuided(IsolatedAsyncioTestCase):
boot_expectations = [
diff --git a/subiquity/server/controllers/tests/test_identity.py b/subiquity/server/controllers/tests/test_identity.py
new file mode 100644
index 000000000..cf5f0896c
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_identity.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.identity import IdentityController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestIdentityController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ IdentityController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(IdentityController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_keyboard.py b/subiquity/server/controllers/tests/test_keyboard.py
index 001cf3e29..9a8c80aec 100644
--- a/subiquity/server/controllers/tests/test_keyboard.py
+++ b/subiquity/server/controllers/tests/test_keyboard.py
@@ -17,6 +17,9 @@
import unittest
from unittest.mock import Mock, patch
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.common.types import KeyboardSetting
from subiquity.models.keyboard import KeyboardModel
from subiquity.server.controllers.keyboard import KeyboardController
@@ -29,6 +32,16 @@ class opts:
dry_run = True
+class TestKeyboardController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ KeyboardController.autoinstall_schema
+ )
+ JsonValidator.check_schema(KeyboardController.autoinstall_schema)
+
+
class TestSubiquityModel(SubiTestCase):
async def test_write_config(self):
os.environ["SUBIQUITY_REPLAY_TIMESCALE"] = "100"
diff --git a/subiquity/server/controllers/tests/test_locale.py b/subiquity/server/controllers/tests/test_locale.py
new file mode 100644
index 000000000..5bf5969ff
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_locale.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.locale import LocaleController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestLocaleController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ LocaleController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(LocaleController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_mirror.py b/subiquity/server/controllers/tests/test_mirror.py
index c707dd825..db88a86fc 100644
--- a/subiquity/server/controllers/tests/test_mirror.py
+++ b/subiquity/server/controllers/tests/test_mirror.py
@@ -19,6 +19,7 @@
from unittest import mock
import jsonschema
+from jsonschema.validators import validator_for
from subiquity.common.types import MirrorSelectionFallback
from subiquity.models.mirror import MirrorModel
@@ -264,3 +265,12 @@ async def test_run_mirror_selection_or_fallback(self):
mock_fallback.assert_not_called()
await controller.run_mirror_selection_or_fallback(context=None)
mock_fallback.assert_called_once()
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ MirrorController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(MirrorController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_network.py b/subiquity/server/controllers/tests/test_network.py
new file mode 100644
index 000000000..761f93b12
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_network.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.network import NetworkController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestNetworkController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ NetworkController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(NetworkController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_oem.py b/subiquity/server/controllers/tests/test_oem.py
index 15832f981..4aa8d7f23 100644
--- a/subiquity/server/controllers/tests/test_oem.py
+++ b/subiquity/server/controllers/tests/test_oem.py
@@ -16,6 +16,9 @@
import subprocess
from unittest.mock import Mock, patch
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.server.controllers.oem import OEMController
from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import make_app
@@ -110,3 +113,12 @@ async def test_wants_oem_kernel_oem(self):
"oem-sutton-balint-meta", context=None, overlay=Mock()
)
)
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ OEMController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(OEMController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_package.py b/subiquity/server/controllers/tests/test_package.py
new file mode 100644
index 000000000..820ed2ecd
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_package.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.package import PackageController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestPackageController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ PackageController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(PackageController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_proxy.py b/subiquity/server/controllers/tests/test_proxy.py
new file mode 100644
index 000000000..2f695b36d
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_proxy.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.proxy import ProxyController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestProxyController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ ProxyController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(ProxyController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_refresh.py b/subiquity/server/controllers/tests/test_refresh.py
index 0da98fbfa..22ca38cc8 100644
--- a/subiquity/server/controllers/tests/test_refresh.py
+++ b/subiquity/server/controllers/tests/test_refresh.py
@@ -15,6 +15,9 @@
from unittest import mock
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.server import snapdapi
from subiquity.server.controllers import refresh as refresh_mod
from subiquity.server.controllers.refresh import RefreshController, SnapChannelSource
@@ -99,3 +102,12 @@ async def GET(self):
await self.rc.configure_snapd(context=self.rc.context)
paw.assert_not_called()
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ RefreshController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(RefreshController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_reporting.py b/subiquity/server/controllers/tests/test_reporting.py
new file mode 100644
index 000000000..56ae4c196
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_reporting.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.reporting import ReportingController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestReportingController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ ReportingController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(ReportingController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_shutdown.py b/subiquity/server/controllers/tests/test_shutdown.py
new file mode 100644
index 000000000..a3d6972cb
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_shutdown.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.shutdown import ShutdownController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestShutdownController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ ShutdownController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(ShutdownController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_snaplist.py b/subiquity/server/controllers/tests/test_snaplist.py
index ba12aed7a..5eecf6764 100644
--- a/subiquity/server/controllers/tests/test_snaplist.py
+++ b/subiquity/server/controllers/tests/test_snaplist.py
@@ -16,13 +16,17 @@
import unittest
from unittest.mock import AsyncMock
+import jsonschema
import requests
+from jsonschema.validators import validator_for
from subiquity.models.snaplist import SnapListModel
from subiquity.server.controllers.snaplist import (
SnapdSnapInfoLoader,
+ SnapListController,
SnapListFetchError,
)
+from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import make_app
@@ -56,3 +60,14 @@ async def test_list_task_completed(self):
await self.loader.get_snap_list_task()
self.assertTrue(self.loader.fetch_list_completed())
self.assertFalse(self.loader.fetch_list_failed())
+
+
+class TestSnapListController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ SnapListController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(SnapListController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_ssh.py b/subiquity/server/controllers/tests/test_ssh.py
index a141cfaf9..8f36c8068 100644
--- a/subiquity/server/controllers/tests/test_ssh.py
+++ b/subiquity/server/controllers/tests/test_ssh.py
@@ -16,6 +16,9 @@
import unittest
from unittest import mock
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.common.types import SSHFetchIdStatus, SSHIdentity
from subiquity.server.controllers.ssh import (
SSHController,
@@ -98,3 +101,12 @@ async def test_fetch_id_GET_fingerprint_error(self):
self.assertEqual(response.status, SSHFetchIdStatus.FINGERPRINT_ERROR)
self.assertEqual(response.error, stderr)
self.assertIsNone(response.identities)
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ SSHController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(SSHController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_timezone.py b/subiquity/server/controllers/tests/test_timezone.py
new file mode 100644
index 000000000..457576e63
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_timezone.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.timezone import TimeZoneController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestTimeZoneController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ TimeZoneController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(TimeZoneController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_ubuntu_pro.py b/subiquity/server/controllers/tests/test_ubuntu_pro.py
index 2de2f31de..c2787a53c 100644
--- a/subiquity/server/controllers/tests/test_ubuntu_pro.py
+++ b/subiquity/server/controllers/tests/test_ubuntu_pro.py
@@ -15,6 +15,9 @@
import unittest
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.server.controllers.ubuntu_pro import UbuntuProController
from subiquity.server.dryrun import DRConfig
from subiquitycore.tests.mocks import make_app
@@ -33,3 +36,12 @@ def test_serialize(self):
def test_deserialize(self):
self.controller.deserialize("1A2B3C4D")
self.assertEqual(self.controller.model.token, "1A2B3C4D")
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ UbuntuProController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(UbuntuProController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_updates.py b/subiquity/server/controllers/tests/test_updates.py
new file mode 100644
index 000000000..de8d21d2d
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_updates.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import jsonschema
+from jsonschema.validators import validator_for
+
+from subiquity.server.controllers.updates import UpdatesController
+from subiquitycore.tests import SubiTestCase
+
+
+class TestUpdatesController(SubiTestCase):
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ UpdatesController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(UpdatesController.autoinstall_schema)
diff --git a/subiquity/server/controllers/tests/test_userdata.py b/subiquity/server/controllers/tests/test_userdata.py
index 9c2f471ec..7ec0d7960 100644
--- a/subiquity/server/controllers/tests/test_userdata.py
+++ b/subiquity/server/controllers/tests/test_userdata.py
@@ -15,7 +15,9 @@
import unittest
+import jsonschema
from cloudinit.config.schema import SchemaValidationError
+from jsonschema.validators import validator_for
from subiquity.server.controllers.userdata import UserdataController
from subiquitycore.tests.mocks import make_app
@@ -58,3 +60,12 @@ def test_load_autoinstall_data(self):
validate.assert_called_with(
data=invalid_schema, data_source="autoinstall.user-data"
)
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ UserdataController.autoinstall_schema
+ )
+
+ JsonValidator.check_schema(UserdataController.autoinstall_schema)
diff --git a/subiquity/server/server.py b/subiquity/server/server.py
index 3d4bd0348..75831878d 100644
--- a/subiquity/server/server.py
+++ b/subiquity/server/server.py
@@ -24,12 +24,13 @@
import yaml
from aiohttp import web
from cloudinit.config.cc_set_passwords import rand_user_password
+from jsonschema.exceptions import ValidationError
from systemd import journal
from subiquity.cloudinit import get_host_combined_cloud_config
from subiquity.common.api.server import bind, controller_for_request
from subiquity.common.apidef import API
-from subiquity.common.errorreport import ErrorReporter, ErrorReportKind
+from subiquity.common.errorreport import ErrorReport, ErrorReporter, ErrorReportKind
from subiquity.common.serialize import to_json
from subiquity.common.types import (
ApplicationState,
@@ -40,6 +41,7 @@
PasswordKind,
)
from subiquity.models.subiquity import ModelNames, SubiquityModel
+from subiquity.server.autoinstall import AutoinstallError, AutoinstallValidationError
from subiquity.server.controller import SubiquityController
from subiquity.server.dryrun import DRConfig
from subiquity.server.errors import ErrorController
@@ -222,6 +224,12 @@ class SubiquityServer(Application):
"minimum": 1,
"maximum": 1,
},
+ "interactive-sections": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ },
+ },
},
"required": ["version"],
"additionalProperties": True,
@@ -395,8 +403,9 @@ def note_data_for_apport(self, key, value):
def make_apport_report(self, kind, thing, *, wait=False, **kw):
return self.error_reporter.make_apport_report(kind, thing, wait=wait, **kw)
- async def _run_error_cmds(self, report):
- await report._info_task
+ async def _run_error_cmds(self, report: Optional[ErrorReport] = None) -> None:
+ if report is not None and report._info_task is not None:
+ await report._info_task
Error = getattr(self.controllers, "Error", None)
if Error is not None and Error.cmds:
try:
@@ -413,7 +422,7 @@ def _exception_handler(self, loop, context):
return
report = self.error_reporter.report_for_exc(exc)
log.error("top level error", exc_info=exc)
- if not report:
+ if not isinstance(exc, AutoinstallError) and not report:
report = self.make_apport_report(
ErrorReportKind.UNKNOWN, "unknown error", exc=exc
)
@@ -466,6 +475,20 @@ async def apply_autoinstall_config(self, context):
await controller.apply_autoinstall_config()
await controller.configured()
+ def validate_autoinstall(self):
+ with self.context.child("core_validation", level="INFO"):
+ try:
+ jsonschema.validate(self.autoinstall_config, self.base_schema)
+ except ValidationError as original_exception:
+ # SubiquityServer currently only checks for these sections
+ # of autoinstall. Hardcode until we have better validation.
+ section = "version or interative-sessions"
+ new_exception: AutoinstallValidationError = AutoinstallValidationError(
+ section,
+ )
+
+ raise new_exception from original_exception
+
def load_autoinstall_config(self, *, only_early):
log.debug(
"load_autoinstall_config only_early %s file %s",
@@ -480,8 +503,7 @@ def load_autoinstall_config(self, *, only_early):
self.controllers.Reporting.setup_autoinstall()
self.controllers.Reporting.start()
self.controllers.Error.setup_autoinstall()
- with self.context.child("core_validation", level="INFO"):
- jsonschema.validate(self.autoinstall_config, self.base_schema)
+ self.validate_autoinstall()
self.controllers.Early.setup_autoinstall()
else:
for controller in self.controllers.instances:
diff --git a/subiquity/server/tests/test_controller.py b/subiquity/server/tests/test_controller.py
index b1c0826c0..89592aa75 100644
--- a/subiquity/server/tests/test_controller.py
+++ b/subiquity/server/tests/test_controller.py
@@ -16,6 +16,7 @@
import contextlib
from unittest.mock import patch
+from subiquity.server.autoinstall import AutoinstallValidationError
from subiquity.server.controller import SubiquityController
from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import make_app
@@ -41,7 +42,6 @@ def test_setup_autoinstall(self, mock_load):
}
self.controller.autoinstall_key = "sample"
self.controller.autoinstall_key_alias = "sample-alias"
- self.controller.autoinstall_default = "default-data"
self.controller.setup_autoinstall()
mock_load.assert_called_once_with("some-sample-data")
@@ -56,5 +56,36 @@ def test_setup_autoinstall(self, mock_load):
mock_load.reset_mock()
self.controller.autoinstall_key = "inexistent"
self.controller.autoinstall_key_alias = "inexistent"
+ self.controller.autoinstall_default = "default-data"
self.controller.setup_autoinstall()
mock_load.assert_called_once_with("default-data")
+
+ def test_autoinstall_validation(self):
+ """Test validation error type and no apport reporting"""
+
+ self.controller.autoinstall_schema = {
+ "type": "object",
+ "properties": {
+ "some-key": {
+ "type": "boolean",
+ },
+ },
+ }
+
+ self.bad_ai_data = {"some-key": "not a bool"}
+
+ self.controller.autoinstall_key = "some-key"
+
+ # Assert error type is correct
+ with self.assertRaises(AutoinstallValidationError) as ctx:
+ self.controller.validate_autoinstall(self.bad_ai_data)
+
+ exception = ctx.exception
+
+ # Assert error section is based on autoinstall_key
+ self.assertEquals(exception.owner, "some-key")
+
+ # Assert apport report is not created
+ # This only checks that controllers do not manually create an apport
+ # report on validation. Should also be tested in Server
+ self.controller.app.make_apport_report.assert_not_called()
diff --git a/subiquity/server/tests/test_server.py b/subiquity/server/tests/test_server.py
index 60c40280d..ce1eb1b09 100644
--- a/subiquity/server/tests/test_server.py
+++ b/subiquity/server/tests/test_server.py
@@ -17,7 +17,11 @@
import shlex
from unittest.mock import Mock, patch
+import jsonschema
+from jsonschema.validators import validator_for
+
from subiquity.common.types import PasswordKind
+from subiquity.server.autoinstall import AutoinstallValidationError
from subiquity.server.server import (
MetaController,
SubiquityServer,
@@ -141,6 +145,56 @@ def test_early_commands_changes_autoinstall(self):
self.assertEqual(after_early, self.server.autoinstall_config)
+class TestAutoinstallValidation(SubiTestCase):
+ async def asyncSetUp(self):
+ opts = Mock()
+ opts.dry_run = True
+ opts.output_base = self.tmp_dir()
+ opts.machine_config = "examples/machines/simple.json"
+ self.server = SubiquityServer(opts, None)
+ self.server.base_schema = {
+ "type": "object",
+ "properties": {
+ "some-key": {
+ "type": "boolean",
+ },
+ },
+ }
+ self.server.make_apport_report = Mock()
+
+ def test_valid_schema(self):
+ """Test that the expected autoinstall JSON schema is valid"""
+
+ JsonValidator: jsonschema.protocols.Validator = validator_for(
+ SubiquityServer.base_schema
+ )
+
+ JsonValidator.check_schema(SubiquityServer.base_schema)
+
+ def test_autoinstall_validation__error_type(self):
+ """Test that bad autoinstall data throws AutoinstallValidationError"""
+
+ bad_ai_data = {"some-key": "not a bool"}
+ self.server.autoinstall_config = bad_ai_data
+
+ with self.assertRaises(AutoinstallValidationError):
+ self.server.validate_autoinstall()
+
+ async def test_autoinstall_validation__no_error_report(self):
+ """Test no apport reporting"""
+
+ exception = AutoinstallValidationError("Mock")
+
+ loop = Mock()
+ context = {"exception": exception}
+
+ with patch("subiquity.server.server.log"):
+ with patch.object(self.server, "_run_error_cmds"):
+ self.server._exception_handler(loop, context)
+
+ self.server.make_apport_report.assert_not_called()
+
+
class TestMetaController(SubiTestCase):
async def test_interactive_sections_not_present(self):
mc = MetaController(make_app())
diff --git a/subiquitycore/tests/mocks.py b/subiquitycore/tests/mocks.py
index 89456ac6d..748b97b55 100644
--- a/subiquitycore/tests/mocks.py
+++ b/subiquitycore/tests/mocks.py
@@ -50,5 +50,6 @@ def make_app(model=None):
app.log_syslog_id = None
app.report_start_event = mock.Mock()
app.report_finish_event = mock.Mock()
+ app.make_apport_report = mock.Mock()
return app