summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2022-01-31 20:45:29 -0700
committerGitHub <noreply@github.com>2022-01-31 20:45:29 -0700
commitaf7eb1deab12c7208853c5d18b55228e0ba29c4d (patch)
treedb4f4b836a972f72aa4fdddf3840c136bc1abb57 /tests
parent46a0126e874927353e83b385b58ab054e58667cc (diff)
downloadvyos-cloud-init-af7eb1deab12c7208853c5d18b55228e0ba29c4d.tar.gz
vyos-cloud-init-af7eb1deab12c7208853c5d18b55228e0ba29c4d.zip
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 <james.falcon@canonical.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/integration_tests/modules/test_ca_certs.py4
-rw-r--r--tests/unittests/config/test_cc_apt_configure.py202
-rw-r--r--tests/unittests/config/test_cc_bootcmd.py100
-rw-r--r--tests/unittests/config/test_cc_byobu.py51
-rw-r--r--tests/unittests/config/test_cc_ca_certs.py106
-rw-r--r--tests/unittests/config/test_cc_chef.py172
-rw-r--r--tests/unittests/config/test_cc_debug.py54
-rw-r--r--tests/unittests/config/test_cc_disable_ec2_metadata.py33
-rw-r--r--tests/unittests/config/test_cc_disk_setup.py50
-rw-r--r--tests/unittests/config/test_schema.py22
10 files changed, 727 insertions, 67 deletions
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):