From af7eb1deab12c7208853c5d18b55228e0ba29c4d Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 31 Jan 2022 20:45:29 -0700 Subject: Schema a d (#1211) Migrate from legacy schema or define new schema in cloud-init-schema.json, adding extensive schema tests for: - cc_apt_configure - cc_bootcmd - cc_byobu - cc_ca_certs - cc_chef - cc_debug - cc_disable_ec2_metadata - cc_disk_setup Deprecate config hyphenated schema keys in favor of underscores: - ca_certs and ca_certs.remove_defaults instead of ca-certs and ca-certs.remove-defaults - Continue to honor deprecated config keys but emit DEPRECATION warnings in logs for continued use of the deprecated keys: - apt_sources key - any apt v1 or v2 keys - use or ca-certs or ca_certs.remove-defaults - Extend apt_configure schema - Define more strict schema below object opaque keys using patternProperties - create common $def apt_configure.mirror for reuse in 'primary' and 'security' schema definitions within cc_apt_configure Co-Authored-by: James Falcon --- tests/integration_tests/modules/test_ca_certs.py | 4 +- tests/unittests/config/test_cc_apt_configure.py | 202 +++++++++++++++++++++ tests/unittests/config/test_cc_bootcmd.py | 100 +++++----- tests/unittests/config/test_cc_byobu.py | 51 ++++++ tests/unittests/config/test_cc_ca_certs.py | 106 ++++++++++- tests/unittests/config/test_cc_chef.py | 172 ++++++++++++++++++ tests/unittests/config/test_cc_debug.py | 54 +++++- .../config/test_cc_disable_ec2_metadata.py | 33 +++- tests/unittests/config/test_cc_disk_setup.py | 50 ++++- tests/unittests/config/test_schema.py | 22 ++- 10 files changed, 727 insertions(+), 67 deletions(-) create mode 100644 tests/unittests/config/test_cc_apt_configure.py create mode 100644 tests/unittests/config/test_cc_byobu.py (limited to 'tests') diff --git a/tests/integration_tests/modules/test_ca_certs.py b/tests/integration_tests/modules/test_ca_certs.py index d514fc62..7247fd7d 100644 --- a/tests/integration_tests/modules/test_ca_certs.py +++ b/tests/integration_tests/modules/test_ca_certs.py @@ -12,8 +12,8 @@ import pytest USER_DATA = """\ #cloud-config -ca-certs: - remove-defaults: true +ca_certs: + remove_defaults: true trusted: - | -----BEGIN CERTIFICATE----- diff --git a/tests/unittests/config/test_cc_apt_configure.py b/tests/unittests/config/test_cc_apt_configure.py new file mode 100644 index 00000000..bd1bb963 --- /dev/null +++ b/tests/unittests/config/test_cc_apt_configure.py @@ -0,0 +1,202 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +""" Tests for cc_apt_configure module """ + +import re + +import pytest + +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import skipUnlessJsonSchema + + +class TestAPTConfigureSchema: + @pytest.mark.parametrize( + "config, error_msg", + ( + # Supplement valid schemas from examples tested in test_schema + ({"apt": {"preserve_sources_list": True}}, None), + # Invalid schemas + ( + {"apt": "nonobject"}, + "apt: 'nonobject' is not of type 'object", + ), + ( + {"apt": {"boguskey": True}}, + re.escape( + "apt: Additional properties are not allowed" + " ('boguskey' was unexpected)" + ), + ), + ({"apt": {}}, "apt: {} does not have enough properties"), + ( + {"apt": {"preserve_sources_list": 1}}, + "apt.preserve_sources_list: 1 is not of type 'boolean'", + ), + ( + {"apt": {"disable_suites": 1}}, + "apt.disable_suites: 1 is not of type 'array'", + ), + ( + {"apt": {"disable_suites": []}}, + re.escape("apt.disable_suites: [] is too short"), + ), + ( + {"apt": {"disable_suites": [1]}}, + "apt.disable_suites.0: 1 is not of type 'string'", + ), + ( + {"apt": {"disable_suites": ["a", "a"]}}, + re.escape( + "apt.disable_suites: ['a', 'a'] has non-unique elements" + ), + ), + # All apt: primary tests are applicable for "security" key too. + # Those apt:security tests are exercised in the unittest below + ( + {"apt": {"primary": "nonlist"}}, + "apt.primary: 'nonlist' is not of type 'array'", + ), + ( + {"apt": {"primary": []}}, + re.escape("apt.primary: [] is too short"), + ), + ( + {"apt": {"primary": ["nonobj"]}}, + "apt.primary.0: 'nonobj' is not of type 'object'", + ), + ( + {"apt": {"primary": [{}]}}, + "apt.primary.0: 'arches' is a required property", + ), + ( + {"apt": {"primary": [{"boguskey": True}]}}, + re.escape( + "apt.primary.0: Additional properties are not allowed" + " ('boguskey' was unexpected)" + ), + ), + ( + {"apt": {"primary": [{"arches": True}]}}, + "apt.primary.0.arches: True is not of type 'array'", + ), + ( + {"apt": {"primary": [{"uri": True}]}}, + "apt.primary.0.uri: True is not of type 'string'", + ), + ( + { + "apt": { + "primary": [ + {"arches": ["amd64"], "search": "non-array"} + ] + } + }, + "apt.primary.0.search: 'non-array' is not of type 'array'", + ), + ( + {"apt": {"primary": [{"arches": ["amd64"], "search": []}]}}, + re.escape("apt.primary.0.search: [] is too short"), + ), + ( + { + "apt": { + "primary": [{"arches": ["amd64"], "search_dns": "a"}] + } + }, + "apt.primary.0.search_dns: 'a' is not of type 'boolean'", + ), + ( + {"apt": {"primary": [{"arches": ["amd64"], "keyid": 1}]}}, + "apt.primary.0.keyid: 1 is not of type 'string'", + ), + ( + {"apt": {"primary": [{"arches": ["amd64"], "key": 1}]}}, + "apt.primary.0.key: 1 is not of type 'string'", + ), + ( + {"apt": {"primary": [{"arches": ["amd64"], "keyserver": 1}]}}, + "apt.primary.0.keyserver: 1 is not of type 'string'", + ), + ( + {"apt": {"add_apt_repo_match": True}}, + "apt.add_apt_repo_match: True is not of type 'string'", + ), + ( + {"apt": {"debconf_selections": True}}, + "apt.debconf_selections: True is not of type 'object'", + ), + ( + {"apt": {"debconf_selections": {}}}, + "apt.debconf_selections: {} does not have enough properties", + ), + ( + {"apt": {"sources_list": True}}, + "apt.sources_list: True is not of type 'string'", + ), + ( + {"apt": {"conf": True}}, + "apt.conf: True is not of type 'string'", + ), + ( + {"apt": {"http_proxy": True}}, + "apt.http_proxy: True is not of type 'string'", + ), + ( + {"apt": {"https_proxy": True}}, + "apt.https_proxy: True is not of type 'string'", + ), + ( + {"apt": {"proxy": True}}, + "apt.proxy: True is not of type 'string'", + ), + ( + {"apt": {"ftp_proxy": True}}, + "apt.ftp_proxy: True is not of type 'string'", + ), + ( + {"apt": {"sources": True}}, + "apt.sources: True is not of type 'object'", + ), + ( + {"apt": {"sources": {"opaquekey": True}}}, + "apt.sources.opaquekey: True is not of type 'object'", + ), + ( + {"apt": {"sources": {"opaquekey": {}}}}, + "apt.sources.opaquekey: {} does not have enough properties", + ), + ( + {"apt": {"sources": {"opaquekey": {"boguskey": True}}}}, + re.escape( + "apt.sources.opaquekey: Additional properties are not" + " allowed ('boguskey' was unexpected)" + ), + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + schema = get_schema() + if error_msg is None: + validate_cloudconfig_schema(config, schema, strict=True) + else: + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + # Note apt['primary'] and apt['security'] have same defition + # Avoid test setup duplicates by running same test using 'security' + if isinstance(config.get("apt"), dict) and config["apt"].get( + "primary" + ): + # To exercise security schema, rename test key from primary + config["apt"]["security"] = config["apt"].pop("primary") + error_msg = error_msg.replace("primary", "security") + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + +# vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_bootcmd.py b/tests/unittests/config/test_cc_bootcmd.py index 17033596..34b16b85 100644 --- a/tests/unittests/config/test_cc_bootcmd.py +++ b/tests/unittests/config/test_cc_bootcmd.py @@ -1,15 +1,18 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging +import re import tempfile +import pytest + from cloudinit import subp, util -from cloudinit.config.cc_bootcmd import handle, schema -from tests.unittests.helpers import ( - CiTestCase, - SchemaTestCaseMixin, - mock, - skipUnlessJsonSchema, +from cloudinit.config.cc_bootcmd import handle +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, ) +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) @@ -65,44 +68,13 @@ class TestBootcmd(CiTestCase): str(context_manager.exception), ) - @skipUnlessJsonSchema() - def test_handler_schema_validation_warns_non_array_type(self): - """Schema validation warns of non-array type for bootcmd key. - - Schema validation is not strict, so bootcmd attempts to shellify the - invalid content. - """ - invalid_config = {"bootcmd": 1} - cc = get_cloud() - with self.assertRaises(TypeError): - handle("cc_bootcmd", invalid_config, cc, LOG, []) - self.assertIn( - "Invalid cloud-config provided:\nbootcmd: 1 is not of type" - " 'array'", - self.logs.getvalue(), - ) - self.assertIn("Failed to shellify", self.logs.getvalue()) - - @skipUnlessJsonSchema() - def test_handler_schema_validation_warns_non_array_item_type(self): - """Schema validation warns of non-array or string bootcmd items. - - Schema validation is not strict, so bootcmd attempts to shellify the - invalid content. - """ invalid_config = { "bootcmd": ["ls /", 20, ["wget", "http://stuff/blah"], {"a": "n"}] } cc = get_cloud() with self.assertRaises(TypeError) as context_manager: handle("cc_bootcmd", invalid_config, cc, LOG, []) - expected_warnings = [ - "bootcmd.1: 20 is not valid under any of the given schemas", - "bootcmd.3: {'a': 'n'} is not valid under any of the given schema", - ] logs = self.logs.getvalue() - for warning in expected_warnings: - self.assertIn(warning, logs) self.assertIn("Failed to shellify", logs) self.assertEqual( "Unable to shellify type 'int'. Expected list, string, tuple. " @@ -146,22 +118,48 @@ class TestBootcmd(CiTestCase): @skipUnlessJsonSchema() -class TestSchema(CiTestCase, SchemaTestCaseMixin): +class TestBootCMDSchema: """Directly test schema rather than through handle.""" - schema = schema - - def test_duplicates_are_fine_array_array(self): - """Duplicated commands array/array entries are allowed.""" - self.assertSchemaValid( - ["byebye", "byebye"], "command entries can be duplicate" - ) - - def test_duplicates_are_fine_array_string(self): - """Duplicated commands array/string entries are allowed.""" - self.assertSchemaValid( - ["echo bye", "echo bye"], "command entries can be duplicate." - ) + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid schemas tested by meta.examples in test_schema + # Invalid schemas + ( + {"bootcmd": 1}, + "Cloud config schema errors: bootcmd: 1 is not of type" + " 'array'", + ), + ({"bootcmd": []}, re.escape("bootcmd: [] is too short")), + ( + {"bootcmd": []}, + re.escape( + "Cloud config schema errors: bootcmd: [] is too short" + ), + ), + ( + { + "bootcmd": [ + "ls /", + 20, + ["wget", "http://stuff/blah"], + {"a": "n"}, + ] + }, + "Cloud config schema errors: bootcmd.1: 20 is not valid under" + " any of the given schemas, bootcmd.3: {'a': 'n'} is not" + " valid under any of the given schemas", + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_byobu.py b/tests/unittests/config/test_cc_byobu.py new file mode 100644 index 00000000..fbdf3403 --- /dev/null +++ b/tests/unittests/config/test_cc_byobu.py @@ -0,0 +1,51 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import re + +import pytest + +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import skipUnlessJsonSchema + + +class TestByobuSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Supplement valid schemas tested by meta.examples in test_schema + ({"byobu_by_default": "enable"}, None), + # Invalid schemas + ( + {"byobu_by_default": 1}, + "byobu_by_default: 1 is not of type 'string'", + ), + ( + {"byobu_by_default": "bogusenum"}, + re.escape( + "byobu_by_default: 'bogusenum' is not one of" + " ['enable-system', 'enable-user', 'disable-system'," + " 'disable-user', 'enable', 'disable'," + " 'user', 'system']" + ), + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + if error_msg is None: + validate_cloudconfig_schema(config, schema, strict=True) + else: + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + +# vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_ca_certs.py b/tests/unittests/config/test_cc_ca_certs.py index c49922e6..39614635 100644 --- a/tests/unittests/config/test_cc_ca_certs.py +++ b/tests/unittests/config/test_cc_ca_certs.py @@ -1,14 +1,22 @@ # This file is part of cloud-init. See LICENSE file for license information. import logging +import re import shutil import tempfile import unittest from contextlib import ExitStack from unittest import mock +import pytest + from cloudinit import distros, helpers, subp, util from cloudinit.config import cc_ca_certs -from tests.unittests.helpers import TestCase +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import TestCase, skipUnlessJsonSchema from tests.unittests.util import get_cloud @@ -128,7 +136,7 @@ class TestConfig(TestCase): def test_remove_default_ca_certs(self): """Test remove_defaults works as expected.""" - config = {"ca-certs": {"remove-defaults": True}} + config = {"ca_certs": {"remove_defaults": True}} for distro_name in cc_ca_certs.distros: self._mock_init() @@ -141,7 +149,7 @@ class TestConfig(TestCase): def test_no_remove_defaults_if_false(self): """Test remove_defaults is not called when config value is False.""" - config = {"ca-certs": {"remove-defaults": False}} + config = {"ca_certs": {"remove_defaults": False}} for distro_name in cc_ca_certs.distros: self._mock_init() @@ -154,7 +162,7 @@ class TestConfig(TestCase): def test_correct_order_for_remove_then_add(self): """Test remove_defaults is not called when config value is False.""" - config = {"ca-certs": {"remove-defaults": True, "trusted": ["CERT1"]}} + config = {"ca_certs": {"remove_defaults": True, "trusted": ["CERT1"]}} for distro_name in cc_ca_certs.distros: self._mock_init() @@ -406,4 +414,94 @@ class TestRemoveDefaultCaCerts(TestCase): ) +class TestCACertsSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid, yet deprecated schemas + ({"ca-certs": {"remove-defaults": True}}, None), + # Invalid schemas + ( + {"ca_certs": 1}, + "ca_certs: 1 is not of type 'object'", + ), + ( + {"ca_certs": {}}, + re.escape("ca_certs: {} does not have enough properties"), + ), + ( + {"ca_certs": {"boguskey": 1}}, + re.escape( + "ca_certs: Additional properties are not allowed" + " ('boguskey' was unexpected)" + ), + ), + ( + {"ca_certs": {"remove_defaults": 1}}, + "ca_certs.remove_defaults: 1 is not of type 'boolean'", + ), + ( + {"ca_certs": {"trusted": [1]}}, + "ca_certs.trusted.0: 1 is not of type 'string'", + ), + ( + {"ca_certs": {"trusted": []}}, + re.escape("ca_certs.trusted: [] is too short"), + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + if error_msg is None: + validate_cloudconfig_schema(config, schema, strict=True) + else: + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + @mock.patch.object(cc_ca_certs, "update_ca_certs") + def test_deprecate_key_warnings(self, update_ca_certs, caplog): + """Assert warnings are logged for deprecated keys.""" + log = logging.getLogger("CALogTest") + cloud = get_cloud("ubuntu") + cc_ca_certs.handle( + "IGNORE", {"ca-certs": {"remove-defaults": False}}, cloud, log, [] + ) + expected_warnings = [ + "DEPRECATION: key 'ca-certs' is now deprecated. Use 'ca_certs'" + " instead.", + "DEPRECATION: key 'ca-certs.remove-defaults' is now deprecated." + " Use 'ca_certs.remove_defaults' instead.", + ] + for warning in expected_warnings: + assert warning in caplog.text + assert 1 == update_ca_certs.call_count + + @mock.patch.object(cc_ca_certs, "update_ca_certs") + def test_duplicate_keys(self, update_ca_certs, caplog): + """Assert warnings are logged for deprecated keys.""" + log = logging.getLogger("CALogTest") + cloud = get_cloud("ubuntu") + cc_ca_certs.handle( + "IGNORE", + { + "ca-certs": {"remove-defaults": True}, + "ca_certs": {"remove_defaults": False}, + }, + cloud, + log, + [], + ) + expected_warning = ( + "Found both ca-certs (deprecated) and ca_certs config keys." + " Ignoring ca-certs." + ) + assert expected_warning in caplog.text + assert 1 == update_ca_certs.call_count + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_chef.py b/tests/unittests/config/test_cc_chef.py index 835974e5..f86be293 100644 --- a/tests/unittests/config/test_cc_chef.py +++ b/tests/unittests/config/test_cc_chef.py @@ -3,17 +3,25 @@ import json import logging import os +import re import httpretty +import pytest from cloudinit import util from cloudinit.config import cc_chef +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests.helpers import ( FilesystemMockingTestCase, HttprettyTestCase, cloud_init_project_dir, mock, skipIf, + skipUnlessJsonSchema, ) from tests.unittests.util import get_cloud @@ -289,4 +297,168 @@ class TestChef(FilesystemMockingTestCase): self.assertEqual(expected_cert, util.load_file(v_path)) +@skipUnlessJsonSchema() +class TestBootCMDSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid schemas tested by meta.examples in test_schema + # Invalid schemas + ( + {"chef": 1}, + "chef: 1 is not of type 'object'", + ), + ( + {"chef": {}}, + re.escape(" chef: {} does not have enough properties"), + ), + ( + {"chef": {"boguskey": True}}, + re.escape( + "chef: Additional properties are not allowed" + " ('boguskey' was unexpected)" + ), + ), + ( + {"chef": {"directories": 1}}, + "chef.directories: 1 is not of type 'array'", + ), + ( + {"chef": {"directories": []}}, + re.escape("chef.directories: [] is too short"), + ), + ( + {"chef": {"directories": [1]}}, + "chef.directories.0: 1 is not of type 'string'", + ), + ( + {"chef": {"directories": ["a", "a"]}}, + re.escape( + "chef.directories: ['a', 'a'] has non-unique elements" + ), + ), + ( + {"chef": {"validation_cert": 1}}, + "chef.validation_cert: 1 is not of type 'string'", + ), + ( + {"chef": {"validation_key": 1}}, + "chef.validation_key: 1 is not of type 'string'", + ), + ( + {"chef": {"firstboot_path": 1}}, + "chef.firstboot_path: 1 is not of type 'string'", + ), + ( + {"chef": {"client_key": 1}}, + "chef.client_key: 1 is not of type 'string'", + ), + ( + {"chef": {"encrypted_data_bag_secret": 1}}, + "chef.encrypted_data_bag_secret: 1 is not of type 'string'", + ), + ( + {"chef": {"environment": 1}}, + "chef.environment: 1 is not of type 'string'", + ), + ( + {"chef": {"file_backup_path": 1}}, + "chef.file_backup_path: 1 is not of type 'string'", + ), + ( + {"chef": {"file_cache_path": 1}}, + "chef.file_cache_path: 1 is not of type 'string'", + ), + ( + {"chef": {"json_attribs": 1}}, + "chef.json_attribs: 1 is not of type 'string'", + ), + ( + {"chef": {"log_level": 1}}, + "chef.log_level: 1 is not of type 'string'", + ), + ( + {"chef": {"log_location": 1}}, + "chef.log_location: 1 is not of type 'string'", + ), + ( + {"chef": {"node_name": 1}}, + "chef.node_name: 1 is not of type 'string'", + ), + ( + {"chef": {"omnibus_url": 1}}, + "chef.omnibus_url: 1 is not of type 'string'", + ), + ( + {"chef": {"omnibus_url_retries": "one"}}, + "chef.omnibus_url_retries: 'one' is not of type 'integer'", + ), + ( + {"chef": {"omnibus_version": 1}}, + "chef.omnibus_version: 1 is not of type 'string'", + ), + ( + {"chef": {"omnibus_version": 1}}, + "chef.omnibus_version: 1 is not of type 'string'", + ), + ( + {"chef": {"pid_file": 1}}, + "chef.pid_file: 1 is not of type 'string'", + ), + ( + {"chef": {"server_url": 1}}, + "chef.server_url: 1 is not of type 'string'", + ), + ( + {"chef": {"show_time": 1}}, + "chef.show_time: 1 is not of type 'boolean'", + ), + ( + {"chef": {"ssl_verify_mode": 1}}, + "chef.ssl_verify_mode: 1 is not of type 'string'", + ), + ( + {"chef": {"validation_name": 1}}, + "chef.validation_name: 1 is not of type 'string'", + ), + ( + {"chef": {"force_install": 1}}, + "chef.force_install: 1 is not of type 'boolean'", + ), + ( + {"chef": {"initial_attributes": 1}}, + "chef.initial_attributes: 1 is not of type 'object'", + ), + ( + {"chef": {"install_type": 1}}, + "chef.install_type: 1 is not of type 'string'", + ), + ( + {"chef": {"install_type": "bogusenum"}}, + re.escape( + "chef.install_type: 'bogusenum' is not one of" + " ['packages', 'gems', 'omnibus']" + ), + ), + ( + {"chef": {"run_list": 1}}, + "chef.run_list: 1 is not of type 'array'", + ), + ( + {"chef": {"chef_license": 1}}, + "chef.chef_license: 1 is not of type 'string'", + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_debug.py b/tests/unittests/config/test_cc_debug.py index 79a88561..fc8d43dc 100644 --- a/tests/unittests/config/test_cc_debug.py +++ b/tests/unittests/config/test_cc_debug.py @@ -2,12 +2,24 @@ # # This file is part of cloud-init. See LICENSE file for license information. import logging +import re import shutil import tempfile +import pytest + from cloudinit import util from cloudinit.config import cc_debug -from tests.unittests.helpers import FilesystemMockingTestCase, mock +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import ( + FilesystemMockingTestCase, + mock, + skipUnlessJsonSchema, +) from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) @@ -57,4 +69,44 @@ class TestDebug(FilesystemMockingTestCase): ) +@skipUnlessJsonSchema() +class TestDebugSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid schemas tested by meta.examples in test_schema + # Invalid schemas + ({"debug": 1}, "debug: 1 is not of type 'object'"), + ( + {"debug": {}}, + re.escape("debug: {} does not have enough properties"), + ), + ( + {"debug": {"boguskey": True}}, + re.escape( + "Additional properties are not allowed ('boguskey' was" + " unexpected)" + ), + ), + ( + {"debug": {"verbose": 1}}, + "debug.verbose: 1 is not of type 'boolean'", + ), + ( + {"debug": {"output": 1}}, + "debug.output: 1 is not of type 'string'", + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_disable_ec2_metadata.py b/tests/unittests/config/test_cc_disable_ec2_metadata.py index 3c3313a7..5755e29e 100644 --- a/tests/unittests/config/test_cc_disable_ec2_metadata.py +++ b/tests/unittests/config/test_cc_disable_ec2_metadata.py @@ -4,8 +4,15 @@ import logging +import pytest + import cloudinit.config.cc_disable_ec2_metadata as ec2_meta -from tests.unittests.helpers import CiTestCase, mock +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema LOG = logging.getLogger(__name__) @@ -47,4 +54,28 @@ class TestEC2MetadataRoute(CiTestCase): m_subp.assert_not_called() +@skipUnlessJsonSchema() +class TestDisableEc2MetadataSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid schemas tested by meta.examples in test_schema + # Invalid schemas + ( + {"disable_ec2_metadata": 1}, + "disable_ec2_metadata: 1 is not of type 'boolean'", + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_disk_setup.py b/tests/unittests/config/test_cc_disk_setup.py index 8a8d7195..f2796e83 100644 --- a/tests/unittests/config/test_cc_disk_setup.py +++ b/tests/unittests/config/test_cc_disk_setup.py @@ -1,9 +1,23 @@ # This file is part of cloud-init. See LICENSE file for license information. import random +import re + +import pytest from cloudinit.config import cc_disk_setup -from tests.unittests.helpers import CiTestCase, ExitStack, TestCase, mock +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import ( + CiTestCase, + ExitStack, + TestCase, + mock, + skipUnlessJsonSchema, +) class TestIsDiskUsed(TestCase): @@ -283,5 +297,37 @@ class TestMkfsCommandHandling(CiTestCase): ) -# +@skipUnlessJsonSchema() +class TestDebugSchema: + """Directly test schema rather than through handle.""" + + @pytest.mark.parametrize( + "config, error_msg", + ( + # Valid schemas tested by meta.examples in test_schema + # Invalid schemas + ({"disk_setup": 1}, "disk_setup: 1 is not of type 'object'"), + ({"fs_setup": 1}, "fs_setup: 1 is not of type 'array'"), + ( + {"device_aliases": 1}, + "device_aliases: 1 is not of type 'object'", + ), + ( + {"debug": {"boguskey": True}}, + re.escape( + "Additional properties are not allowed ('boguskey' was" + " unexpected)" + ), + ), + ), + ) + @skipUnlessJsonSchema() + def test_schema_validation(self, config, error_msg): + """Assert expected schema validation and error messages.""" + # New-style schema $defs exist in config/cloud-init-schema*.json + schema = get_schema() + with pytest.raises(SchemaValidationError, match=error_msg): + validate_cloudconfig_schema(config, schema, strict=True) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index 1647f6e5..2f43d9e7 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -91,6 +91,13 @@ class TestGetSchema: "cc_apt_configure", "cc_apt_pipelining", "cc_bootcmd", + "cc_byobu", + "cc_ca_certs", + "cc_chef", + "cc_debug", + "cc_disable_ec2_metadata", + "cc_disk_setup", + "cc_install_hotplug", "cc_keyboard", "cc_locale", "cc_ntp", @@ -101,8 +108,6 @@ class TestGetSchema: "cc_ubuntu_drivers", "cc_write_files", "cc_zypper_add_repo", - "cc_chef", - "cc_install_hotplug", ] ) == sorted( [meta["id"] for meta in get_metas().values() if meta is not None] @@ -112,7 +117,15 @@ class TestGetSchema: # New style schema should be defined in static schema file in $defs expected_subschema_defs = [ {"$ref": "#/$defs/cc_apk_configure"}, + {"$ref": "#/$defs/cc_apt_configure"}, {"$ref": "#/$defs/cc_apt_pipelining"}, + {"$ref": "#/$defs/cc_bootcmd"}, + {"$ref": "#/$defs/cc_byobu"}, + {"$ref": "#/$defs/cc_ca_certs"}, + {"$ref": "#/$defs/cc_chef"}, + {"$ref": "#/$defs/cc_debug"}, + {"$ref": "#/$defs/cc_disable_ec2_metadata"}, + {"$ref": "#/$defs/cc_disk_setup"}, ] found_subschema_defs = [] legacy_schema_keys = [] @@ -125,9 +138,6 @@ class TestGetSchema: assert expected_subschema_defs == found_subschema_defs # This list will dwindle as we move legacy schema to new $defs assert [ - "apt", - "bootcmd", - "chef", "drivers", "keyboard", "locale", @@ -153,7 +163,7 @@ class TestLoadDoc: "module_name", ( "cc_apt_pipelining", # new style composite schema file - "cc_bootcmd", # legacy sub-schema defined in module + "cc_zypper_add_repo", # legacy sub-schema defined in module ), ) def test_report_docs_for_legacy_and_consolidated_schema(self, module_name): -- cgit v1.2.3