3
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
5
# Author: Avishai Ish-Shalom <avishai@fewbytes.com>
6
# Author: Mike Moulton <mike@meltmedia.com>
7
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
9
# This program is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License version 3, as
11
# published by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
**Summary:** module that configures, starts and installs chef.
24
**Description:** This module enables chef to be installed (from packages or
25
from gems, or from omnibus). Before this occurs chef configurations are
26
written to disk (validation.pem, client.pem, firstboot.json, client.rb),
27
and needed chef folders/directories are created (/etc/chef and /var/log/chef
28
and so-on). Then once installing proceeds correctly if configured chef will
29
be started (in daemon mode or in non-daemon mode) and then once that has
30
finished (if ran in non-daemon mode this will be when chef finishes
31
converging, if ran in daemon mode then no further actions are possible since
32
chef will have forked into its own process) then a post run function can
33
run that can do finishing activities (such as removing the validation pem
36
It can be configured with the following option structure::
39
directories: (defaulting to /etc/chef, /var/log/chef, /var/lib/chef,
40
/var/cache/chef, /var/backups/chef, /var/run/chef)
41
validation_cert: (optional string to be written to file validation_key)
42
special value 'system' means set use existing file
43
validation_key: (optional the path for validation_cert. default
44
/etc/chef/validation.pem)
45
firstboot_path: (path to write run_list and initial_attributes keys that
46
should also be present in this configuration, defaults
47
to /etc/chef/firstboot.json)
48
exec: boolean to run or not run chef (defaults to false, unless
49
a gem installed is requested
50
where this will then default
53
chef.rb template keys (if falsey, then will be skipped and not
54
written to /etc/chef/client.rb)
78
from cloudinit import templater
79
from cloudinit import url_helper
80
from cloudinit import util
84
RUBY_VERSION_DEFAULT = "1.8"
94
REQUIRED_CHEF_DIRS = tuple([
98
# Used if fetching chef from a omnibus style package
99
OMNIBUS_URL = "https://www.getchef.com/chef/install.sh"
100
OMNIBUS_URL_RETRIES = 5
102
CHEF_VALIDATION_PEM_PATH = '/etc/chef/validation.pem'
103
CHEF_FB_PATH = '/etc/chef/firstboot.json'
104
CHEF_RB_TPL_DEFAULTS = {
105
# These are ruby symbols...
106
'ssl_verify_mode': ':verify_none',
107
'log_level': ':info',
108
# These are not symbols...
109
'log_location': '/var/log/chef/client.log',
110
'validation_key': CHEF_VALIDATION_PEM_PATH,
111
'validation_cert': None,
112
'client_key': "/etc/chef/client.pem",
113
'json_attribs': CHEF_FB_PATH,
114
'file_cache_path': "/var/cache/chef",
115
'file_backup_path': "/var/backups/chef",
116
'pid_file': "/var/run/chef/client.pid",
119
CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])
120
CHEF_RB_TPL_PATH_KEYS = frozenset([
129
CHEF_RB_TPL_KEYS = list(CHEF_RB_TPL_DEFAULTS.keys())
130
CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_BOOL_KEYS)
131
CHEF_RB_TPL_KEYS.extend(CHEF_RB_TPL_PATH_KEYS)
132
CHEF_RB_TPL_KEYS.extend([
138
CHEF_RB_TPL_KEYS = frozenset(CHEF_RB_TPL_KEYS)
139
CHEF_RB_PATH = '/etc/chef/client.rb'
140
CHEF_EXEC_PATH = '/usr/bin/chef-client'
141
CHEF_EXEC_DEF_ARGS = tuple(['-d', '-i', '1800', '-s', '20'])
145
if not os.path.isfile(CHEF_EXEC_PATH):
147
if not os.access(CHEF_EXEC_PATH, os.X_OK):
152
def post_run_chef(chef_cfg, log):
153
delete_pem = util.get_cfg_option_bool(chef_cfg,
154
'delete_validation_post_exec',
156
if delete_pem and os.path.isfile(CHEF_VALIDATION_PEM_PATH):
157
os.unlink(CHEF_VALIDATION_PEM_PATH)
160
def get_template_params(iid, chef_cfg, log):
161
params = CHEF_RB_TPL_DEFAULTS.copy()
162
# Allow users to overwrite any of the keys they want (if they so choose),
163
# when a value is None, then the value will be set to None and no boolean
164
# or string version will be populated...
165
for (k, v) in chef_cfg.items():
166
if k not in CHEF_RB_TPL_KEYS:
167
log.debug("Skipping unknown chef template key '%s'", k)
172
# This will make the value a boolean or string...
173
if k in CHEF_RB_TPL_BOOL_KEYS:
174
params[k] = util.get_cfg_option_bool(chef_cfg, k)
176
params[k] = util.get_cfg_option_str(chef_cfg, k)
177
# These ones are overwritten to be exact values...
179
'generated_by': util.make_header(),
180
'node_name': util.get_cfg_option_str(chef_cfg, 'node_name',
182
'environment': util.get_cfg_option_str(chef_cfg, 'environment',
184
# These two are mandatory...
185
'server_url': chef_cfg['server_url'],
186
'validation_name': chef_cfg['validation_name'],
191
def handle(name, cfg, cloud, log, _args):
192
"""Handler method activated by cloud-init."""
194
# If there isn't a chef key in the configuration don't do anything
195
if 'chef' not in cfg:
196
log.debug(("Skipping module named %s,"
197
" no 'chef' key in configuration"), name)
199
chef_cfg = cfg['chef']
201
# Ensure the chef directories we use exist
202
chef_dirs = util.get_cfg_option_list(chef_cfg, 'directories')
204
chef_dirs = list(CHEF_DIRS)
205
for d in itertools.chain(chef_dirs, REQUIRED_CHEF_DIRS):
208
vkey_path = chef_cfg.get('validation_key', CHEF_VALIDATION_PEM_PATH)
209
vcert = chef_cfg.get('validation_cert')
210
# special value 'system' means do not overwrite the file
211
# but still render the template to contain 'validation_key'
213
if vcert != "system":
214
util.write_file(vkey_path, vcert)
215
elif not os.path.isfile(vkey_path):
216
log.warn("chef validation_cert provided as 'system', but "
217
"validation_key path '%s' does not exist.",
220
# Create the chef config from template
221
template_fn = cloud.get_template_filename('chef_client.rb')
223
iid = str(cloud.datasource.get_instance_id())
224
params = get_template_params(iid, chef_cfg, log)
225
# Do a best effort attempt to ensure that the template values that
226
# are associated with paths have there parent directory created
227
# before they are used by the chef-client itself.
229
for (k, v) in params.items():
230
if k in CHEF_RB_TPL_PATH_KEYS and v:
231
param_paths.add(os.path.dirname(v))
232
util.ensure_dirs(param_paths)
233
templater.render_to_file(template_fn, CHEF_RB_PATH, params)
235
log.warn("No template found, not rendering to %s",
238
# Set the firstboot json
239
fb_filename = util.get_cfg_option_str(chef_cfg, 'firstboot_path',
240
default=CHEF_FB_PATH)
242
log.info("First boot path empty, not writing first boot json file")
245
if 'run_list' in chef_cfg:
246
initial_json['run_list'] = chef_cfg['run_list']
247
if 'initial_attributes' in chef_cfg:
248
initial_attributes = chef_cfg['initial_attributes']
249
for k in list(initial_attributes.keys()):
250
initial_json[k] = initial_attributes[k]
251
util.write_file(fb_filename, json.dumps(initial_json))
253
# Try to install chef, if its not already installed...
254
force_install = util.get_cfg_option_bool(chef_cfg,
255
'force_install', default=False)
256
if not is_installed() or force_install:
257
run = install_chef(cloud, chef_cfg, log)
259
run = util.get_cfg_option_bool(chef_cfg, 'exec', default=False)
263
run_chef(chef_cfg, log)
264
post_run_chef(chef_cfg, log)
267
def run_chef(chef_cfg, log):
268
log.debug('Running chef-client')
269
cmd = [CHEF_EXEC_PATH]
270
if 'exec_arguments' in chef_cfg:
271
cmd_args = chef_cfg['exec_arguments']
272
if isinstance(cmd_args, (list, tuple)):
274
elif isinstance(cmd_args, six.string_types):
277
log.warn("Unknown type %s provided for chef"
278
" 'exec_arguments' expected list, tuple,"
279
" or string", type(cmd_args))
280
cmd.extend(CHEF_EXEC_DEF_ARGS)
282
cmd.extend(CHEF_EXEC_DEF_ARGS)
283
util.subp(cmd, capture=False)
286
def install_chef(cloud, chef_cfg, log):
287
# If chef is not installed, we install chef based on 'install_type'
288
install_type = util.get_cfg_option_str(chef_cfg, 'install_type',
290
run = util.get_cfg_option_bool(chef_cfg, 'exec', default=False)
291
if install_type == "gems":
292
# This will install and run the chef-client from gems
293
chef_version = util.get_cfg_option_str(chef_cfg, 'version', None)
294
ruby_version = util.get_cfg_option_str(chef_cfg, 'ruby_version',
295
RUBY_VERSION_DEFAULT)
296
install_chef_from_gems(ruby_version, chef_version, cloud.distro)
297
# Retain backwards compat, by preferring True instead of False
298
# when not provided/overriden...
299
run = util.get_cfg_option_bool(chef_cfg, 'exec', default=True)
300
elif install_type == 'packages':
301
# This will install and run the chef-client from packages
302
cloud.distro.install_packages(('chef',))
303
elif install_type == 'omnibus':
304
# This will install as a omnibus unified package
305
url = util.get_cfg_option_str(chef_cfg, "omnibus_url", OMNIBUS_URL)
306
retries = max(0, util.get_cfg_option_int(chef_cfg,
307
"omnibus_url_retries",
308
default=OMNIBUS_URL_RETRIES))
309
content = url_helper.readurl(url=url, retries=retries)
310
with util.tempdir() as tmpd:
311
# Use tmpdir over tmpfile to avoid 'text file busy' on execute
312
tmpf = "%s/chef-omnibus-install" % tmpd
313
util.write_file(tmpf, content, mode=0o700)
314
util.subp([tmpf], capture=False)
316
log.warn("Unknown chef install type '%s'", install_type)
321
def get_ruby_packages(version):
322
# return a list of packages needed to install ruby at version
323
pkgs = ['ruby%s' % version, 'ruby%s-dev' % version]
325
pkgs.extend(('libopenssl-ruby1.8', 'rubygems1.8'))
329
def install_chef_from_gems(ruby_version, chef_version, distro):
330
distro.install_packages(get_ruby_packages(ruby_version))
331
if not os.path.exists('/usr/bin/gem'):
332
util.sym_link('/usr/bin/gem%s' % ruby_version, '/usr/bin/gem')
333
if not os.path.exists('/usr/bin/ruby'):
334
util.sym_link('/usr/bin/ruby%s' % ruby_version, '/usr/bin/ruby')
336
util.subp(['/usr/bin/gem', 'install', 'chef',
337
'-v %s' % chef_version, '--no-ri',
338
'--no-rdoc', '--bindir', '/usr/bin', '-q'], capture=False)
340
util.subp(['/usr/bin/gem', 'install', 'chef',
341
'--no-ri', '--no-rdoc', '--bindir',
342
'/usr/bin', '-q'], capture=False)