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