Skip to content

Commit

Permalink
schema: migrate legacy cc_chef schema to cloud-init-schema
Browse files Browse the repository at this point in the history
Migrate legacy chef schema to new cloud-init-schea.json.
Add more strict schema definition disallowing additionalProperties.
Add extensive unittests for invalid schemas.
  • Loading branch information
blackboxsw committed Jan 25, 2022
1 parent 72c20c9 commit 0e0f207
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 5 deletions.
5 changes: 2 additions & 3 deletions cloudinit/config/cc_chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -433,7 +433,7 @@
},
}

__doc__ = get_meta_doc(meta, schema)
__doc__ = get_meta_doc(meta)


def post_run_chef(chef_cfg, log):
Expand Down Expand Up @@ -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
Expand Down
151 changes: 150 additions & 1 deletion cloudinit/config/cloud-init-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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" }
]
}
172 changes: 172 additions & 0 deletions tests/unittests/config/test_cc_chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Loading

0 comments on commit 0e0f207

Please sign in to comment.