diff options
Diffstat (limited to 'tests/unittests/config/test_cc_chef.py')
-rw-r--r-- | tests/unittests/config/test_cc_chef.py | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/tests/unittests/config/test_cc_chef.py b/tests/unittests/config/test_cc_chef.py new file mode 100644 index 00000000..f86be293 --- /dev/null +++ b/tests/unittests/config/test_cc_chef.py @@ -0,0 +1,464 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +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 + +LOG = logging.getLogger(__name__) + +CLIENT_TEMPL = cloud_init_project_dir("templates/chef_client.rb.tmpl") + +# This is adjusted to use http because using with https causes issue +# in some openssl/httpretty combinations. +# https://github.com/gabrielfalcao/HTTPretty/issues/242 +# We saw issue in opensuse 42.3 with +# httpretty=0.8.8-7.1 ndg-httpsclient=0.4.0-3.2 pyOpenSSL=16.0.0-4.1 +OMNIBUS_URL_HTTP = cc_chef.OMNIBUS_URL.replace("https:", "http:") + + +class TestInstallChefOmnibus(HttprettyTestCase): + def setUp(self): + super(TestInstallChefOmnibus, self).setUp() + self.new_root = self.tmp_dir() + + @mock.patch("cloudinit.config.cc_chef.OMNIBUS_URL", OMNIBUS_URL_HTTP) + def test_install_chef_from_omnibus_runs_chef_url_content(self): + """install_chef_from_omnibus calls subp_blob_in_tempfile.""" + response = b'#!/bin/bash\necho "Hi Mom"' + httpretty.register_uri( + httpretty.GET, cc_chef.OMNIBUS_URL, body=response, status=200 + ) + ret = (None, None) # stdout, stderr but capture=False + + with mock.patch( + "cloudinit.config.cc_chef.subp_blob_in_tempfile", return_value=ret + ) as m_subp_blob: + cc_chef.install_chef_from_omnibus() + # admittedly whitebox, but assuming subp_blob_in_tempfile works + # this should be fine. + self.assertEqual( + [ + mock.call( + blob=response, + args=[], + basename="chef-omnibus-install", + capture=False, + ) + ], + m_subp_blob.call_args_list, + ) + + @mock.patch("cloudinit.config.cc_chef.url_helper.readurl") + @mock.patch("cloudinit.config.cc_chef.subp_blob_in_tempfile") + def test_install_chef_from_omnibus_retries_url(self, m_subp_blob, m_rdurl): + """install_chef_from_omnibus retries OMNIBUS_URL upon failure.""" + + class FakeURLResponse(object): + contents = '#!/bin/bash\necho "Hi Mom" > {0}/chef.out'.format( + self.new_root + ) + + m_rdurl.return_value = FakeURLResponse() + + cc_chef.install_chef_from_omnibus() + expected_kwargs = { + "retries": cc_chef.OMNIBUS_URL_RETRIES, + "url": cc_chef.OMNIBUS_URL, + } + self.assertCountEqual(expected_kwargs, m_rdurl.call_args_list[0][1]) + cc_chef.install_chef_from_omnibus(retries=10) + expected_kwargs = {"retries": 10, "url": cc_chef.OMNIBUS_URL} + self.assertCountEqual(expected_kwargs, m_rdurl.call_args_list[1][1]) + expected_subp_kwargs = { + "args": ["-v", "2.0"], + "basename": "chef-omnibus-install", + "blob": m_rdurl.return_value.contents, + "capture": False, + } + self.assertCountEqual( + expected_subp_kwargs, m_subp_blob.call_args_list[0][1] + ) + + @mock.patch("cloudinit.config.cc_chef.OMNIBUS_URL", OMNIBUS_URL_HTTP) + @mock.patch("cloudinit.config.cc_chef.subp_blob_in_tempfile") + def test_install_chef_from_omnibus_has_omnibus_version(self, m_subp_blob): + """install_chef_from_omnibus provides version arg to OMNIBUS_URL.""" + chef_outfile = self.tmp_path("chef.out", self.new_root) + response = '#!/bin/bash\necho "Hi Mom" > {0}'.format(chef_outfile) + httpretty.register_uri( + httpretty.GET, cc_chef.OMNIBUS_URL, body=response + ) + cc_chef.install_chef_from_omnibus(omnibus_version="2.0") + + called_kwargs = m_subp_blob.call_args_list[0][1] + expected_kwargs = { + "args": ["-v", "2.0"], + "basename": "chef-omnibus-install", + "blob": response, + "capture": False, + } + self.assertCountEqual(expected_kwargs, called_kwargs) + + +class TestChef(FilesystemMockingTestCase): + def setUp(self): + super(TestChef, self).setUp() + self.tmp = self.tmp_dir() + + def test_no_config(self): + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + cfg = {} + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + for d in cc_chef.CHEF_DIRS: + self.assertFalse(os.path.isdir(d)) + + @skipIf( + not os.path.isfile(CLIENT_TEMPL), CLIENT_TEMPL + " is not available" + ) + def test_basic_config(self): + """ + test basic config looks sane + + # This should create a file of the format... + # Created by cloud-init v. 0.7.6 on Sat, 11 Oct 2014 23:57:21 +0000 + chef_license "accept" + log_level :info + ssl_verify_mode :verify_none + log_location "/var/log/chef/client.log" + validation_client_name "bob" + validation_key "/etc/chef/validation.pem" + client_key "/etc/chef/client.pem" + chef_server_url "localhost" + environment "_default" + node_name "iid-datasource-none" + json_attribs "/etc/chef/firstboot.json" + file_cache_path "/var/cache/chef" + file_backup_path "/var/backups/chef" + pid_file "/var/run/chef/client.pid" + Chef::Log::Formatter.show_time = true + encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret" + """ + tpl_file = util.load_file(CLIENT_TEMPL) + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + util.write_file("/etc/cloud/templates/chef_client.rb.tmpl", tpl_file) + cfg = { + "chef": { + "chef_license": "accept", + "server_url": "localhost", + "validation_name": "bob", + "validation_key": "/etc/chef/vkey.pem", + "validation_cert": "this is my cert", + "encrypted_data_bag_secret": ( + "/etc/chef/encrypted_data_bag_secret" + ), + }, + } + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + for d in cc_chef.CHEF_DIRS: + self.assertTrue(os.path.isdir(d)) + c = util.load_file(cc_chef.CHEF_RB_PATH) + + # the content of these keys is not expected to be rendered to tmpl + unrendered_keys = ("validation_cert",) + for k, v in cfg["chef"].items(): + if k in unrendered_keys: + continue + self.assertIn(v, c) + for k, v in cc_chef.CHEF_RB_TPL_DEFAULTS.items(): + if k in unrendered_keys: + continue + # the value from the cfg overrides that in the default + val = cfg["chef"].get(k, v) + if isinstance(val, str): + self.assertIn(val, c) + c = util.load_file(cc_chef.CHEF_FB_PATH) + self.assertEqual({}, json.loads(c)) + + def test_firstboot_json(self): + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + cfg = { + "chef": { + "server_url": "localhost", + "validation_name": "bob", + "run_list": ["a", "b", "c"], + "initial_attributes": { + "c": "d", + }, + }, + } + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + c = util.load_file(cc_chef.CHEF_FB_PATH) + self.assertEqual( + { + "run_list": ["a", "b", "c"], + "c": "d", + }, + json.loads(c), + ) + + @skipIf( + not os.path.isfile(CLIENT_TEMPL), CLIENT_TEMPL + " is not available" + ) + def test_template_deletes(self): + tpl_file = util.load_file(CLIENT_TEMPL) + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + util.write_file("/etc/cloud/templates/chef_client.rb.tmpl", tpl_file) + cfg = { + "chef": { + "server_url": "localhost", + "validation_name": "bob", + "json_attribs": None, + "show_time": None, + }, + } + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + c = util.load_file(cc_chef.CHEF_RB_PATH) + self.assertNotIn("json_attribs", c) + self.assertNotIn("Formatter.show_time", c) + + @skipIf( + not os.path.isfile(CLIENT_TEMPL), CLIENT_TEMPL + " is not available" + ) + def test_validation_cert_and_validation_key(self): + # test validation_cert content is written to validation_key path + tpl_file = util.load_file(CLIENT_TEMPL) + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + util.write_file("/etc/cloud/templates/chef_client.rb.tmpl", tpl_file) + v_path = "/etc/chef/vkey.pem" + v_cert = "this is my cert" + cfg = { + "chef": { + "server_url": "localhost", + "validation_name": "bob", + "validation_key": v_path, + "validation_cert": v_cert, + }, + } + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + content = util.load_file(cc_chef.CHEF_RB_PATH) + self.assertIn(v_path, content) + util.load_file(v_path) + self.assertEqual(v_cert, util.load_file(v_path)) + + def test_validation_cert_with_system(self): + # test validation_cert content is not written over system file + tpl_file = util.load_file(CLIENT_TEMPL) + self.patchUtils(self.tmp) + self.patchOS(self.tmp) + + v_path = "/etc/chef/vkey.pem" + v_cert = "system" + expected_cert = "this is the system file certificate" + cfg = { + "chef": { + "server_url": "localhost", + "validation_name": "bob", + "validation_key": v_path, + "validation_cert": v_cert, + }, + } + util.write_file("/etc/cloud/templates/chef_client.rb.tmpl", tpl_file) + util.write_file(v_path, expected_cert) + cc_chef.handle("chef", cfg, get_cloud(), LOG, []) + content = util.load_file(cc_chef.CHEF_RB_PATH) + self.assertIn(v_path, content) + util.load_file(v_path) + 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 |