From 0e0f207d954350aea2177f11596c5fd046878b53 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 24 Jan 2022 20:24:28 -0700 Subject: [PATCH] schema: migrate legacy cc_chef schema to cloud-init-schema Migrate legacy chef schema to new cloud-init-schea.json. Add more strict schema definition disallowing additionalProperties. Add extensive unittests for invalid schemas. --- cloudinit/config/cc_chef.py | 5 +- cloudinit/config/cloud-init-schema.json | 151 ++++++++++++++++++++- tests/unittests/config/test_cc_chef.py | 172 ++++++++++++++++++++++++ tests/unittests/config/test_schema.py | 3 +- 4 files changed, 326 insertions(+), 5 deletions(-) diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py index 67889683373..ba8b119301a 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -14,7 +14,7 @@ from textwrap import dedent from cloudinit import subp, temp_utils, templater, url_helper, util -from cloudinit.config.schema import get_meta_doc, validate_cloudconfig_schema +from cloudinit.config.schema import get_meta_doc from cloudinit.settings import PER_ALWAYS RUBY_VERSION_DEFAULT = "1.8" @@ -433,7 +433,7 @@ }, } -__doc__ = get_meta_doc(meta, schema) +__doc__ = get_meta_doc(meta) def post_run_chef(chef_cfg, log): @@ -489,7 +489,6 @@ def handle(name, cfg, cloud, log, _args): ) return - validate_cloudconfig_schema(cfg, schema) chef_cfg = cfg["chef"] # Ensure the chef directories we use exist diff --git a/cloudinit/config/cloud-init-schema.json b/cloudinit/config/cloud-init-schema.json index ce8b1213a3b..311e7a0f4a3 100644 --- a/cloudinit/config/cloud-init-schema.json +++ b/cloudinit/config/cloud-init-schema.json @@ -244,6 +244,154 @@ "minProperties": 1 } } + }, + "cc_chef": { + "type": "object", + "properties": { + "chef": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "directories": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "uniqueItems": true, + "description": "Create the necessary directories for chef to run. By default, it creates the following directories:\n\n - ``/etc/chef``\n - ``/var/log/chef``\n - ``/var/lib/chef``\n - ``/var/cache/chef``\n - ``/var/backups/chef``\n - ``/var/run/chef``" + }, + "validation_cert": { + "type": "string", + "description": "Optional string to be written to file validation_key. Special value ``system`` means set use existing file." + }, + "validation_key": { + "type": "string", + "default": "/etc/chef/validation.pem", + "description": "Optional path for validation_cert. default to ``/etc/chef/validation.pem``" + }, + "firstboot_path": { + "type": "string", + "default": "/etc/chef/firstboot.json", + "description": "Path to write run_list and initial_attributes keys that should also be present in this configuration, defaults to ``/etc/chef/firstboot.json``" + }, + "exec": { + "type": "boolean", + "default": false, + "description": "Set true if we should run or not run chef (defaults to false, unless a gem installed is requested where this will then default to true)." + }, + "client_key": { + "type": "string", + "default": "/etc/chef/client.pem", + "description": "Optional path for client_cert. Default to ``/etc/chef/client.pem``." + }, + "encrypted_data_bag_secret": { + "type": "string", + "default": null, + "description": "Specifies the location of the secret key used by chef to encrypt data items. By default, this path is set to null, meaning that chef will have to look at the path ``/etc/chef/encrypted_data_bag_secret`` for it." + }, + "environment": { + "type": "string", + "default": "_default", + "description": "Specifies which environment chef will use. By default, it will use the ``_default`` configuration." + }, + "file_backup_path": { + "type": "string", + "default": "/var/backups/chef", + "description": "Specifies the location in which backup files are stored. By default, it uses the ``/var/backups/chef`` location." + }, + "file_cache_path": { + "type": "string", + "default": "/var/cache/chef", + "description": "Specifies the location in which chef cache files will be saved. By default, it uses the ``/var/cache/chef`` location." + }, + "json_attribs": { + "type": "string", + "default": "/etc/chef/firstboot.json", + "description": "Specifies the location in which some chef json data is stored. By default, it uses the ``/etc/chef/firstboot.json`` location." + }, + "log_level": { + "type": "string", + "default": ":info", + "description": "Defines the level of logging to be stored in the log file. By default this value is set to ``:info``." + }, + "log_location": { + "type": "string", + "default": "/var/log/chef/client.log", + "description": "Specifies the location of the chef lof file. By default, the location is specified at ``/var/log/chef/client.log``." + }, + "node_name": { + "type": "string", + "description": "The name of the node to run. By default, we will use th instance id as the node name." + }, + "omnibus_url": { + "type": "string", + "default": "https://www.chef.io/chef/install.sh", + "description": "Omnibus URL if chef should be installed through Omnibus. By default, it uses the ``https://www.chef.io/chef/install.sh``." + }, + "omnibus_url_retries": { + "type": "integer", + "default": 5, + "description": "The number of retries that will be attempted to reach the Omnibus URL. Default is 5." + }, + "omnibus_version": { + "type": "string", + "description": "Optional version string to require for omnibus install." + }, + "pid_file": { + "type": "string", + "default": "/var/run/chef/client.pid", + "description": "The location in which a process identification number (pid) is saved. By default, it saves in the ``/var/run/chef/client.pid`` location." + }, + "server_url": { + "type": "string", + "description": "The URL for the chef server" + }, + "show_time": { + "type": "boolean", + "default": true, + "description": "Show time in chef logs" + }, + "ssl_verify_mode": { + "type": "string", + "default": ":verify_none", + "description": "Set the verify mode for HTTPS requests. We can have two possible values for this parameter:\n\n - ``:verify_none``: No validation of SSL certificates.\n - ``:verify_peer``: Validate all SSL certificates.\n\nBy default, the parameter is set as ``:verify_none``." + }, + "validation_name": { + "type": "string", + "description": "The name of the chef-validator key that Chef Infra Client uses to access the Chef Infra Server during the initial Chef Infra Client run." + }, + "force_install": { + "type": "boolean", + "default": false, + "description": "If set to ``true``, forces chef installation, even if it is already installed." + }, + "initial_attributes": { + "type": "object", + "items": {"type": "string"}, + "description": "Specify a list of initial attributes used by the cookbooks." + }, + "install_type": { + "type": "string", + "default": "packages", + "enum": [ + "packages", + "gems", + "omnibus" + ], + "description": "The type of installation for chef. It can be one of the following values:\n\n - ``packages``\n - ``gems``\n - ``omnibus``" + }, + "run_list": { + "type": "array", + "items": {"type": "string"}, + "description": "A run list for a first boot json." + }, + "chef_license": { + "type": "string", + "description": "string that indicates if user accepts or not license related to some of chef products" + } + } + } + } } }, "allOf": [ @@ -252,6 +400,7 @@ { "$ref": "#/$defs/cc_apt_pipelining" }, { "$ref": "#/$defs/cc_bootcmd" }, { "$ref": "#/$defs/cc_byobu" }, - { "$ref": "#/$defs/cc_ca_certs" } + { "$ref": "#/$defs/cc_ca_certs" }, + { "$ref": "#/$defs/cc_chef" } ] } diff --git a/tests/unittests/config/test_cc_chef.py b/tests/unittests/config/test_cc_chef.py index 835974e52f8..f86be293b91 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 @@ def test_validation_cert_with_system(self): 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_schema.py b/tests/unittests/config/test_schema.py index c5b50d66411..008969e6b71 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -93,6 +93,7 @@ def test_get_schema_coalesces_known_schema(self): "cc_bootcmd", "cc_byobu", "cc_ca_certs", + "cc_chef", "cc_keyboard", "cc_locale", "cc_ntp", @@ -103,7 +104,6 @@ def test_get_schema_coalesces_known_schema(self): "cc_ubuntu_drivers", "cc_write_files", "cc_zypper_add_repo", - "cc_chef", "cc_install_hotplug", ] ) == sorted( @@ -119,6 +119,7 @@ def test_get_schema_coalesces_known_schema(self): {"$ref": "#/$defs/cc_bootcmd"}, {"$ref": "#/$defs/cc_byobu"}, {"$ref": "#/$defs/cc_ca_certs"}, + {"$ref": "#/$defs/cc_chef"}, ] found_subschema_defs = [] legacy_schema_keys = []