From 20759840b95252c17299ae69a8a5f830ba6fba0e Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Tue, 23 Jan 2024 14:10:11 -0800 Subject: [PATCH 1/5] controller tests: move setting autoinstall_default Move the assignment of autoinstall_default to the relevant block where it's being tested. --- subiquity/server/tests/test_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subiquity/server/tests/test_controller.py b/subiquity/server/tests/test_controller.py index b1c0826c0..b5af4fbbf 100644 --- a/subiquity/server/tests/test_controller.py +++ b/subiquity/server/tests/test_controller.py @@ -41,7 +41,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 +55,6 @@ 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") From f6a7819ef9932ff45edbe6338f38137ff51fbb31 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Wed, 24 Jan 2024 15:47:24 -0800 Subject: [PATCH 2/5] autoinstall: unittests to validate autoinstall schema Provided autoinstall data is already validated against a specific schema by the relevant controller, but these schemas themselves are never validated against a meta schema to guarantee they are valid JSON schemas. This patch adds unit tests for each controller, as well as SubiquityServer, to validate their autoinstall schema against the latest JSON meta schema draft (that is supported in jsonschema). A particular draft validator can be used instead by specifying meta schema in the schema definition. --- .../server/controllers/tests/test_cmdlist.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_codecs.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_debconf.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_drivers.py | 31 +++++++++++++++++++ .../controllers/tests/test_filesystem.py | 11 +++++++ .../server/controllers/tests/test_identity.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_keyboard.py | 13 ++++++++ .../server/controllers/tests/test_locale.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_mirror.py | 10 ++++++ .../server/controllers/tests/test_network.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_oem.py | 12 +++++++ .../server/controllers/tests/test_package.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_proxy.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_refresh.py | 12 +++++++ .../controllers/tests/test_reporting.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_shutdown.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_snaplist.py | 15 +++++++++ .../server/controllers/tests/test_ssh.py | 12 +++++++ .../server/controllers/tests/test_timezone.py | 31 +++++++++++++++++++ .../controllers/tests/test_ubuntu_pro.py | 12 +++++++ .../server/controllers/tests/test_updates.py | 31 +++++++++++++++++++ .../server/controllers/tests/test_userdata.py | 11 +++++++ subiquity/server/tests/test_server.py | 21 +++++++++++++ 23 files changed, 532 insertions(+) create mode 100644 subiquity/server/controllers/tests/test_cmdlist.py create mode 100644 subiquity/server/controllers/tests/test_codecs.py create mode 100644 subiquity/server/controllers/tests/test_debconf.py create mode 100644 subiquity/server/controllers/tests/test_drivers.py create mode 100644 subiquity/server/controllers/tests/test_identity.py create mode 100644 subiquity/server/controllers/tests/test_locale.py create mode 100644 subiquity/server/controllers/tests/test_network.py create mode 100644 subiquity/server/controllers/tests/test_package.py create mode 100644 subiquity/server/controllers/tests/test_proxy.py create mode 100644 subiquity/server/controllers/tests/test_reporting.py create mode 100644 subiquity/server/controllers/tests/test_shutdown.py create mode 100644 subiquity/server/controllers/tests/test_timezone.py create mode 100644 subiquity/server/controllers/tests/test_updates.py 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/tests/test_server.py b/subiquity/server/tests/test_server.py index 60c40280d..4193abcd3 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -17,6 +17,9 @@ 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.server import ( MetaController, @@ -141,6 +144,24 @@ 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) + + 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) + + class TestMetaController(SubiTestCase): async def test_interactive_sections_not_present(self): mc = MetaController(make_app()) From e29a2a6c110957ada4a77948f757b613968770d6 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Thu, 25 Jan 2024 15:52:58 -0800 Subject: [PATCH 3/5] SubiquityServer: Include interactive-sections as a property SubiquityServer is responsible for checking and loading the interactive-sessions, so it should be responsible for validating this section as well. Additionally add this to the autoinstall schema in the docs. --- autoinstall-schema.json | 6 ++++++ autoinstall-system-setup-schema.json | 6 ++++++ subiquity/server/server.py | 6 ++++++ 3 files changed, 18 insertions(+) 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/server.py b/subiquity/server/server.py index 3d4bd0348..99e65aca3 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -222,6 +222,12 @@ class SubiquityServer(Application): "minimum": 1, "maximum": 1, }, + "interactive-sections": { + "type": "array", + "items": { + "type": "string", + }, + }, }, "required": ["version"], "additionalProperties": True, From f1944dd2f742a7253a6af4655c51d1857aa533e7 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Thu, 1 Feb 2024 10:30:46 -0800 Subject: [PATCH 4/5] AutoinstallValidationError: An error for autoinstall validation failures Adds a base exception type for Autoinstall related failures and a specific implementation for autoinstall validation failures. When a user passes incorrect autoinstall data, the installer will crash with an AutoinstallValidationError exception. Failure messages are currently in the form of: "Malformed autoinstall in '' section" where is the name of the top-level key a particular controller is responsible (e.g., 'apt' and MirrorController). The section reporting is a little crude in the validation of the base schema done by SubiquityServer, which can't discern between the 'interative-sessions' and 'version' keys, but for now the scope is pretty limited and can be fixed up at a later time. --- subiquity/server/controller.py | 18 +++++++++++++++- subiquity/server/server.py | 19 +++++++++++++++-- subiquity/server/tests/test_controller.py | 26 +++++++++++++++++++++++ subiquity/server/tests/test_server.py | 18 ++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) 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/server.py b/subiquity/server/server.py index 99e65aca3..1aa949be1 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -24,6 +24,7 @@ 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 @@ -40,6 +41,7 @@ PasswordKind, ) from subiquity.models.subiquity import ModelNames, SubiquityModel +from subiquity.server.autoinstall import AutoinstallValidationError from subiquity.server.controller import SubiquityController from subiquity.server.dryrun import DRConfig from subiquity.server.errors import ErrorController @@ -472,6 +474,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", @@ -486,8 +502,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 b5af4fbbf..bf73c4536 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 @@ -58,3 +59,28 @@ def test_setup_autoinstall(self, mock_load): 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""" + + 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") diff --git a/subiquity/server/tests/test_server.py b/subiquity/server/tests/test_server.py index 4193abcd3..ae380bf2a 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -21,6 +21,7 @@ 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, @@ -151,6 +152,14 @@ async def asyncSetUp(self): 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", + }, + }, + } def test_valid_schema(self): """Test that the expected autoinstall JSON schema is valid""" @@ -161,6 +170,15 @@ def test_valid_schema(self): 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() + class TestMetaController(SubiTestCase): async def test_interactive_sections_not_present(self): From 24de248cec9c4687c3fea856ed030fcddb860b75 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Thu, 1 Feb 2024 10:32:00 -0800 Subject: [PATCH 5/5] AutoinstallError: Disable apport reporting Autoinstall related failures are more likely than not going to be user caused, so we shouldn't immediately generate a crash report for these types of failures. This should hopefully allow the user to debug their autoinstall data much faster and reduce the number of autoinstall-related bugs reported. --- subiquity/server/autoinstall.py | 31 +++++++++++++++++++++++ subiquity/server/server.py | 11 ++++---- subiquity/server/tests/test_controller.py | 7 ++++- subiquity/server/tests/test_server.py | 15 +++++++++++ subiquitycore/tests/mocks.py | 1 + 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 subiquity/server/autoinstall.py 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/server.py b/subiquity/server/server.py index 1aa949be1..75831878d 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -30,7 +30,7 @@ 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, @@ -41,7 +41,7 @@ PasswordKind, ) from subiquity.models.subiquity import ModelNames, SubiquityModel -from subiquity.server.autoinstall import AutoinstallValidationError +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 @@ -403,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: @@ -421,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 ) diff --git a/subiquity/server/tests/test_controller.py b/subiquity/server/tests/test_controller.py index bf73c4536..89592aa75 100644 --- a/subiquity/server/tests/test_controller.py +++ b/subiquity/server/tests/test_controller.py @@ -61,7 +61,7 @@ def test_setup_autoinstall(self, mock_load): mock_load.assert_called_once_with("default-data") def test_autoinstall_validation(self): - """Test validation error type""" + """Test validation error type and no apport reporting""" self.controller.autoinstall_schema = { "type": "object", @@ -84,3 +84,8 @@ def test_autoinstall_validation(self): # 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 ae380bf2a..ce1eb1b09 100644 --- a/subiquity/server/tests/test_server.py +++ b/subiquity/server/tests/test_server.py @@ -160,6 +160,7 @@ async def asyncSetUp(self): }, }, } + self.server.make_apport_report = Mock() def test_valid_schema(self): """Test that the expected autoinstall JSON schema is valid""" @@ -179,6 +180,20 @@ def test_autoinstall_validation__error_type(self): 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): 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