# This file is part of cloud-init. See LICENSE file for license information. import json import logging import os import httpretty from cloudinit import util from cloudinit.config import cc_chef from tests.unittests.helpers import ( FilesystemMockingTestCase, HttprettyTestCase, cloud_init_project_dir, mock, skipIf, ) 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)) # vi: ts=4 expandtab