cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #00533
[Merge] lp:~harlowja/cloud-init/better-chef-module into lp:cloud-init
Joshua Harlow has proposed merging lp:~harlowja/cloud-init/better-chef-module into lp:cloud-init.
Requested reviews:
cloud init development team (cloud-init-dev)
For more details, see:
https://code.launchpad.net/~harlowja/cloud-init/better-chef-module/+merge/238040
Increase the robustness of the chef module
Add the following adjustments to the chef template and module
- Make it so that the chef directories can be provided (defaults
to the existing directories)
- Make the params much more configurable, and if a parameter is
provided in the chef configuration it will override existing template
parameters.
- Make the template skip lines if the values are None in the configuration
so that template lines can be removed if/when this is desirable.
- Allow the firstboot json path to be configurable (defaults to the
existing location).
- Adds a basic set of tests to ensure that good things are happening.
--
https://code.launchpad.net/~harlowja/cloud-init/better-chef-module/+merge/238040
Your team cloud init development team is requested to review the proposed merge of lp:~harlowja/cloud-init/better-chef-module into lp:cloud-init.
=== modified file 'cloudinit/config/cc_chef.py'
--- cloudinit/config/cc_chef.py 2014-08-26 18:50:11 +0000
+++ cloudinit/config/cc_chef.py 2014-10-11 00:35:26 +0000
@@ -18,6 +18,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from datetime import datetime
+
import json
import os
@@ -38,6 +40,61 @@
OMNIBUS_URL = "https://www.opscode.com/chef/install.sh"
+CHEF_RB_TPL_DEFAULTS = {
+ # These are ruby symbols...
+ 'ssl_verify_mode': ':verify_none',
+ 'log_level': ':info',
+ # These are not symbols...
+ 'log_location': '/var/log/chef/client.log',
+ 'validation_key': "/etc/chef/validation.pem",
+ 'client_key': "/etc/chef/client.pem",
+ '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",
+ 'show_time': True,
+}
+CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])
+CHEF_RB_TPL_KEYS = list(CHEF_RB_TPL_DEFAULTS.keys())
+CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_BOOL_KEYS)
+CHEF_RB_TPL_KEYS.extend([
+ 'server_url',
+ 'node_name',
+ 'environment',
+ 'validation_name',
+])
+CHEF_RB_TPL_KEYS = frozenset(CHEF_RB_TPL_KEYS)
+CHEF_RB_PATH = '/etc/chef/client.rb'
+CHEF_FB_PATH = '/etc/chef/firstboot.json'
+
+
+def get_template_params(iid, chef_cfg, log):
+ params = CHEF_RB_TPL_DEFAULTS.copy()
+ params.update({
+ 'server_url': chef_cfg['server_url'],
+ 'node_name': util.get_cfg_option_str(chef_cfg, 'node_name', iid),
+ 'environment': util.get_cfg_option_str(chef_cfg, 'environment',
+ '_default'),
+ 'validation_name': chef_cfg['validation_name'],
+ })
+ # Allow users to overwrite any of the keys they want (if they so choose),
+ # when a value is None, then the value will be set to None and no boolean
+ # or string version will be populated...
+ for (k, v) in chef_cfg.items():
+ if k not in CHEF_RB_TPL_KEYS:
+ log.debug("Skipping unknown chef template key '%s'", k)
+ continue
+ if v is None:
+ params[k] = None
+ else:
+ # This will make the value a boolean or string...
+ if k in CHEF_RB_TPL_BOOL_KEYS:
+ params[k] = util.get_cfg_option_bool(chef_cfg, k)
+ else:
+ params[k] = util.get_cfg_option_str(chef_cfg, k)
+ params['generated_on'] = datetime.now().isoformat()
+ return params
+
def handle(name, cfg, cloud, log, _args):
@@ -49,7 +106,7 @@
chef_cfg = cfg['chef']
# Ensure the chef directories we use exist
- for d in CHEF_DIRS:
+ for d in chef_cfg.get('directories', CHEF_DIRS):
util.ensure_dir(d)
# Set the validation key based on the presence of either 'validation_key'
@@ -64,26 +121,26 @@
template_fn = cloud.get_template_filename('chef_client.rb')
if template_fn:
iid = str(cloud.datasource.get_instance_id())
- params = {
- 'server_url': chef_cfg['server_url'],
- 'node_name': util.get_cfg_option_str(chef_cfg, 'node_name', iid),
- 'environment': util.get_cfg_option_str(chef_cfg, 'environment',
- '_default'),
- 'validation_name': chef_cfg['validation_name']
- }
- templater.render_to_file(template_fn, '/etc/chef/client.rb', params)
+ params = get_template_params(iid, chef_cfg, log)
+ templater.render_to_file(template_fn, CHEF_RB_PATH, params)
else:
- log.warn("No template found, not rendering to /etc/chef/client.rb")
+ log.warn("No template found, not rendering to %s",
+ CHEF_RB_PATH)
- # set the firstboot json
- initial_json = {}
- if 'run_list' in chef_cfg:
- initial_json['run_list'] = chef_cfg['run_list']
- if 'initial_attributes' in chef_cfg:
- initial_attributes = chef_cfg['initial_attributes']
- for k in list(initial_attributes.keys()):
- initial_json[k] = initial_attributes[k]
- util.write_file('/etc/chef/firstboot.json', json.dumps(initial_json))
+ # Set the firstboot json
+ fb_filename = util.get_cfg_option_str(chef_cfg, 'firstboot_path',
+ default=CHEF_FB_PATH)
+ if not fb_filename:
+ log.info("First boot path empty, not writing first boot json file")
+ else:
+ initial_json = {}
+ if 'run_list' in chef_cfg:
+ initial_json['run_list'] = chef_cfg['run_list']
+ if 'initial_attributes' in chef_cfg:
+ initial_attributes = chef_cfg['initial_attributes']
+ for k in list(initial_attributes.keys()):
+ initial_json[k] = initial_attributes[k]
+ util.write_file(fb_filename, json.dumps(initial_json))
# If chef is not installed, we install chef based on 'install_type'
if (not os.path.isfile('/usr/bin/chef-client') or
=== modified file 'cloudinit/util.py'
--- cloudinit/util.py 2014-09-30 20:24:54 +0000
+++ cloudinit/util.py 2014-10-11 00:35:26 +0000
@@ -2007,3 +2007,4 @@
raise ValueError("'%s': cannot be negative" % size_in)
return int(num * mpliers[mplier])
+
=== modified file 'templates/chef_client.rb.tmpl'
--- templates/chef_client.rb.tmpl 2014-03-05 23:05:59 +0000
+++ templates/chef_client.rb.tmpl 2014-10-11 00:35:26 +0000
@@ -9,17 +9,51 @@
validation_name: XYZ
server_url: XYZ
-#}
-log_level :info
-log_location "/var/log/chef/client.log"
-ssl_verify_mode :verify_none
+
+{#
+The reason these are not in quotes is because they are ruby
+symbols that will be placed inside here, and not actual strings...
+#}
+# This is a generated file, created on {{generated_on}}.
+{% if log_level %}
+log_level {{log_level}}
+{% endif %}
+{% if ssl_verify_mode %}
+ssl_verify_mode {{ssl_verify_mode}}
+{% endif %}
+{% if log_location %}
+log_location "{{log_location}}"
+{% endif %}
+{% if validation_name %}
validation_client_name "{{validation_name}}"
-validation_key "/etc/chef/validation.pem"
-client_key "/etc/chef/client.pem"
+{% endif %}
+{% if validation_key %}
+validation_key "{{validation_key}}"
+{% endif %}
+{% if client_key %}
+client_key "{{client_key}}"
+{% endif %}
+{% if server_url %}
chef_server_url "{{server_url}}"
+{% endif %}
+{% if environment %}
environment "{{environment}}"
+{% endif %}
+{% if node_name %}
node_name "{{node_name}}"
-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"
+{% endif %}
+{% if json_attribs %}
+json_attribs "{{json_attribs}}"
+{% endif %}
+{% if file_cache_path %}
+file_cache_path "{{file_cache_path}}"
+{% endif %}
+{% if file_backup_path %}
+file_backup_path "{{file_backup_path}}"
+{% endif %}
+{% if pid_file %}
+pid_file "{{pid_file}}"
+{% endif %}
+{% if show_time %}
Chef::Log::Formatter.show_time = true
+{% endif %}
=== added file 'tests/unittests/test_handler/test_handler_chef.py'
--- tests/unittests/test_handler/test_handler_chef.py 1970-01-01 00:00:00 +0000
+++ tests/unittests/test_handler/test_handler_chef.py 2014-10-11 00:35:26 +0000
@@ -0,0 +1,84 @@
+import os
+import json
+
+from cloudinit.config import cc_chef
+
+from cloudinit import cloud
+from cloudinit import distros
+from cloudinit import helpers
+from cloudinit import util
+from cloudinit.sources import DataSourceNone
+
+from .. import helpers as t_help
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class TestChef(t_help.FilesystemMockingTestCase):
+ def setUp(self):
+ super(TestChef, self).setUp()
+ self.tmp = self.makeDir(prefix="unittest_")
+
+ def fetch_cloud(self, distro_kind):
+ cls = distros.fetch(distro_kind)
+ paths = helpers.Paths({})
+ distro = cls(distro_kind, {}, paths)
+ ds = DataSourceNone.DataSourceNone({}, distro, paths, None)
+ return cloud.Cloud(ds, paths, {}, distro, None)
+
+ def test_no_config(self):
+ self.patchUtils(self.tmp)
+ self.patchOS(self.tmp)
+
+ cfg = {}
+ cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])
+ for d in cc_chef.CHEF_DIRS:
+ self.assertFalse(os.path.isdir(d))
+
+ def test_basic_config(self):
+ tpl_file = util.load_file('templates/chef_client.rb.tmpl')
+ 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',
+ },
+ }
+ cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])
+ for d in cc_chef.CHEF_DIRS:
+ self.assertTrue(os.path.isdir(d))
+ c = util.load_file(cc_chef.CHEF_RB_PATH)
+ for k, v in cfg['chef'].items():
+ self.assertIn(v, c)
+ for k, v in cc_chef.CHEF_RB_TPL_DEFAULTS.items():
+ if isinstance(v, basestring):
+ self.assertIn(v, 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, self.fetch_cloud('ubuntu'), LOG, [])
+ c = util.load_file(cc_chef.CHEF_FB_PATH)
+ self.assertEqual(
+ {
+ 'run_list': ['a', 'b', 'c'],
+ 'c': 'd',
+ }, json.loads(c))
Follow ups