~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/config/cc_chef.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
#    Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
4
 
#
5
 
#    Author: Avishai Ish-Shalom <avishai@fewbytes.com>
6
 
#    Author: Mike Moulton <mike@meltmedia.com>
7
 
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
8
 
#
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.
12
 
#
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.
17
 
#
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/>.
20
 
 
21
 
"""
22
 
**Summary:** module that configures, starts and installs chef.
23
 
 
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
34
 
file).
35
 
 
36
 
It can be configured with the following option structure::
37
 
 
38
 
    chef:
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
51
 
                                             to true)
52
 
 
53
 
    chef.rb template keys (if falsey, then will be skipped and not
54
 
                           written to /etc/chef/client.rb)
55
 
 
56
 
    chef:
57
 
      client_key:
58
 
      environment:
59
 
      file_backup_path:
60
 
      file_cache_path:
61
 
      json_attribs:
62
 
      log_level:
63
 
      log_location:
64
 
      node_name:
65
 
      pid_file:
66
 
      server_url:
67
 
      show_time:
68
 
      ssl_verify_mode:
69
 
      validation_cert:
70
 
      validation_key:
71
 
      validation_name:
72
 
"""
73
 
 
74
 
import itertools
75
 
import json
76
 
import os
77
 
 
78
 
from cloudinit import templater
79
 
from cloudinit import url_helper
80
 
from cloudinit import util
81
 
 
82
 
import six
83
 
 
84
 
RUBY_VERSION_DEFAULT = "1.8"
85
 
 
86
 
CHEF_DIRS = tuple([
87
 
    '/etc/chef',
88
 
    '/var/log/chef',
89
 
    '/var/lib/chef',
90
 
    '/var/cache/chef',
91
 
    '/var/backups/chef',
92
 
    '/var/run/chef',
93
 
])
94
 
REQUIRED_CHEF_DIRS = tuple([
95
 
    '/etc/chef',
96
 
])
97
 
 
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
101
 
 
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",
117
 
    'show_time': True,
118
 
}
119
 
CHEF_RB_TPL_BOOL_KEYS = frozenset(['show_time'])
120
 
CHEF_RB_TPL_PATH_KEYS = frozenset([
121
 
    'log_location',
122
 
    'validation_key',
123
 
    'client_key',
124
 
    'file_cache_path',
125
 
    'json_attribs',
126
 
    'file_cache_path',
127
 
    'pid_file',
128
 
])
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([
133
 
    'server_url',
134
 
    'node_name',
135
 
    'environment',
136
 
    'validation_name',
137
 
])
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'])
142
 
 
143
 
 
144
 
def is_installed():
145
 
    if not os.path.isfile(CHEF_EXEC_PATH):
146
 
        return False
147
 
    if not os.access(CHEF_EXEC_PATH, os.X_OK):
148
 
        return False
149
 
    return True
150
 
 
151
 
 
152
 
def post_run_chef(chef_cfg, log):
153
 
    delete_pem = util.get_cfg_option_bool(chef_cfg,
154
 
                                          'delete_validation_post_exec',
155
 
                                          default=False)
156
 
    if delete_pem and os.path.isfile(CHEF_VALIDATION_PEM_PATH):
157
 
        os.unlink(CHEF_VALIDATION_PEM_PATH)
158
 
 
159
 
 
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)
168
 
            continue
169
 
        if v is None:
170
 
            params[k] = None
171
 
        else:
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)
175
 
            else:
176
 
                params[k] = util.get_cfg_option_str(chef_cfg, k)
177
 
    # These ones are overwritten to be exact values...
178
 
    params.update({
179
 
        'generated_by': util.make_header(),
180
 
        'node_name': util.get_cfg_option_str(chef_cfg, 'node_name',
181
 
                                             default=iid),
182
 
        'environment': util.get_cfg_option_str(chef_cfg, 'environment',
183
 
                                               default='_default'),
184
 
        # These two are mandatory...
185
 
        'server_url': chef_cfg['server_url'],
186
 
        'validation_name': chef_cfg['validation_name'],
187
 
    })
188
 
    return params
189
 
 
190
 
 
191
 
def handle(name, cfg, cloud, log, _args):
192
 
    """Handler method activated by cloud-init."""
193
 
 
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)
198
 
        return
199
 
    chef_cfg = cfg['chef']
200
 
 
201
 
    # Ensure the chef directories we use exist
202
 
    chef_dirs = util.get_cfg_option_list(chef_cfg, 'directories')
203
 
    if not chef_dirs:
204
 
        chef_dirs = list(CHEF_DIRS)
205
 
    for d in itertools.chain(chef_dirs, REQUIRED_CHEF_DIRS):
206
 
        util.ensure_dir(d)
207
 
 
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'
212
 
    if vcert:
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.",
218
 
                     vkey_path)
219
 
 
220
 
    # Create the chef config from template
221
 
    template_fn = cloud.get_template_filename('chef_client.rb')
222
 
    if template_fn:
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.
228
 
        param_paths = set()
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)
234
 
    else:
235
 
        log.warn("No template found, not rendering to %s",
236
 
                 CHEF_RB_PATH)
237
 
 
238
 
    # Set the firstboot json
239
 
    fb_filename = util.get_cfg_option_str(chef_cfg, 'firstboot_path',
240
 
                                          default=CHEF_FB_PATH)
241
 
    if not fb_filename:
242
 
        log.info("First boot path empty, not writing first boot json file")
243
 
    else:
244
 
        initial_json = {}
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))
252
 
 
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)
258
 
    elif is_installed():
259
 
        run = util.get_cfg_option_bool(chef_cfg, 'exec', default=False)
260
 
    else:
261
 
        run = False
262
 
    if run:
263
 
        run_chef(chef_cfg, log)
264
 
        post_run_chef(chef_cfg, log)
265
 
 
266
 
 
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)):
273
 
            cmd.extend(cmd_args)
274
 
        elif isinstance(cmd_args, six.string_types):
275
 
            cmd.append(cmd_args)
276
 
        else:
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)
281
 
    else:
282
 
        cmd.extend(CHEF_EXEC_DEF_ARGS)
283
 
    util.subp(cmd, capture=False)
284
 
 
285
 
 
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',
289
 
                                           'packages')
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)
315
 
    else:
316
 
        log.warn("Unknown chef install type '%s'", install_type)
317
 
        run = False
318
 
    return run
319
 
 
320
 
 
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]
324
 
    if version == "1.8":
325
 
        pkgs.extend(('libopenssl-ruby1.8', 'rubygems1.8'))
326
 
    return pkgs
327
 
 
328
 
 
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')
335
 
    if chef_version:
336
 
        util.subp(['/usr/bin/gem', 'install', 'chef',
337
 
                   '-v %s' % chef_version, '--no-ri',
338
 
                   '--no-rdoc', '--bindir', '/usr/bin', '-q'], capture=False)
339
 
    else:
340
 
        util.subp(['/usr/bin/gem', 'install', 'chef',
341
 
                   '--no-ri', '--no-rdoc', '--bindir',
342
 
                   '/usr/bin', '-q'], capture=False)