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

« back to all changes in this revision

Viewing changes to cloudinit/stages.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 Canonical Ltd.
4
 
#    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
5
 
#    Copyright (C) 2012 Yahoo! Inc.
6
 
#
7
 
#    Author: Scott Moser <scott.moser@canonical.com>
8
 
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
9
 
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
10
 
#
11
 
#    This program is free software: you can redistribute it and/or modify
12
 
#    it under the terms of the GNU General Public License version 3, as
13
 
#    published by the Free Software Foundation.
14
 
#
15
 
#    This program is distributed in the hope that it will be useful,
16
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 
#    GNU General Public License for more details.
19
 
#
20
 
#    You should have received a copy of the GNU General Public License
21
 
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 
 
23
 
import copy
24
 
import os
25
 
import sys
26
 
 
27
 
import six
28
 
from six.moves import cPickle as pickle
29
 
 
30
 
from cloudinit.settings import (PER_INSTANCE, FREQUENCIES, CLOUD_CONFIG)
31
 
 
32
 
from cloudinit import handlers
33
 
 
34
 
# Default handlers (used if not overridden)
35
 
from cloudinit.handlers import boot_hook as bh_part
36
 
from cloudinit.handlers import cloud_config as cc_part
37
 
from cloudinit.handlers import shell_script as ss_part
38
 
from cloudinit.handlers import upstart_job as up_part
39
 
 
40
 
from cloudinit import cloud
41
 
from cloudinit import config
42
 
from cloudinit import distros
43
 
from cloudinit import helpers
44
 
from cloudinit import importer
45
 
from cloudinit import log as logging
46
 
from cloudinit import net
47
 
from cloudinit.net import cmdline
48
 
from cloudinit.reporting import events
49
 
from cloudinit import sources
50
 
from cloudinit import type_utils
51
 
from cloudinit import util
52
 
 
53
 
LOG = logging.getLogger(__name__)
54
 
 
55
 
NULL_DATA_SOURCE = None
56
 
NO_PREVIOUS_INSTANCE_ID = "NO_PREVIOUS_INSTANCE_ID"
57
 
 
58
 
 
59
 
class Init(object):
60
 
    def __init__(self, ds_deps=None, reporter=None):
61
 
        if ds_deps is not None:
62
 
            self.ds_deps = ds_deps
63
 
        else:
64
 
            self.ds_deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK]
65
 
        # Created on first use
66
 
        self._cfg = None
67
 
        self._paths = None
68
 
        self._distro = None
69
 
        # Changed only when a fetch occurs
70
 
        self.datasource = NULL_DATA_SOURCE
71
 
        self.ds_restored = False
72
 
        self._previous_iid = None
73
 
 
74
 
        if reporter is None:
75
 
            reporter = events.ReportEventStack(
76
 
                name="init-reporter", description="init-desc",
77
 
                reporting_enabled=False)
78
 
        self.reporter = reporter
79
 
 
80
 
    def _reset(self, reset_ds=False):
81
 
        # Recreated on access
82
 
        self._cfg = None
83
 
        self._paths = None
84
 
        self._distro = None
85
 
        if reset_ds:
86
 
            self.datasource = NULL_DATA_SOURCE
87
 
            self.ds_restored = False
88
 
 
89
 
    @property
90
 
    def distro(self):
91
 
        if not self._distro:
92
 
            # Try to find the right class to use
93
 
            system_config = self._extract_cfg('system')
94
 
            distro_name = system_config.pop('distro', 'ubuntu')
95
 
            distro_cls = distros.fetch(distro_name)
96
 
            LOG.debug("Using distro class %s", distro_cls)
97
 
            self._distro = distro_cls(distro_name, system_config, self.paths)
98
 
            # If we have an active datasource we need to adjust
99
 
            # said datasource and move its distro/system config
100
 
            # from whatever it was to a new set...
101
 
            if self.datasource is not NULL_DATA_SOURCE:
102
 
                self.datasource.distro = self._distro
103
 
                self.datasource.sys_cfg = system_config
104
 
        return self._distro
105
 
 
106
 
    @property
107
 
    def cfg(self):
108
 
        return self._extract_cfg('restricted')
109
 
 
110
 
    def _extract_cfg(self, restriction):
111
 
        # Ensure actually read
112
 
        self.read_cfg()
113
 
        # Nobody gets the real config
114
 
        ocfg = copy.deepcopy(self._cfg)
115
 
        if restriction == 'restricted':
116
 
            ocfg.pop('system_info', None)
117
 
        elif restriction == 'system':
118
 
            ocfg = util.get_cfg_by_path(ocfg, ('system_info',), {})
119
 
        elif restriction == 'paths':
120
 
            ocfg = util.get_cfg_by_path(ocfg, ('system_info', 'paths'), {})
121
 
        if not isinstance(ocfg, (dict)):
122
 
            ocfg = {}
123
 
        return ocfg
124
 
 
125
 
    @property
126
 
    def paths(self):
127
 
        if not self._paths:
128
 
            path_info = self._extract_cfg('paths')
129
 
            self._paths = helpers.Paths(path_info, self.datasource)
130
 
        return self._paths
131
 
 
132
 
    def _initial_subdirs(self):
133
 
        c_dir = self.paths.cloud_dir
134
 
        initial_dirs = [
135
 
            c_dir,
136
 
            os.path.join(c_dir, 'scripts'),
137
 
            os.path.join(c_dir, 'scripts', 'per-instance'),
138
 
            os.path.join(c_dir, 'scripts', 'per-once'),
139
 
            os.path.join(c_dir, 'scripts', 'per-boot'),
140
 
            os.path.join(c_dir, 'scripts', 'vendor'),
141
 
            os.path.join(c_dir, 'seed'),
142
 
            os.path.join(c_dir, 'instances'),
143
 
            os.path.join(c_dir, 'handlers'),
144
 
            os.path.join(c_dir, 'sem'),
145
 
            os.path.join(c_dir, 'data'),
146
 
        ]
147
 
        return initial_dirs
148
 
 
149
 
    def purge_cache(self, rm_instance_lnk=False):
150
 
        rm_list = []
151
 
        rm_list.append(self.paths.boot_finished)
152
 
        if rm_instance_lnk:
153
 
            rm_list.append(self.paths.instance_link)
154
 
        for f in rm_list:
155
 
            util.del_file(f)
156
 
        return len(rm_list)
157
 
 
158
 
    def initialize(self):
159
 
        self._initialize_filesystem()
160
 
 
161
 
    def _initialize_filesystem(self):
162
 
        util.ensure_dirs(self._initial_subdirs())
163
 
        log_file = util.get_cfg_option_str(self.cfg, 'def_log_file')
164
 
        if log_file:
165
 
            util.ensure_file(log_file)
166
 
            perms = self.cfg.get('syslog_fix_perms')
167
 
            if not perms:
168
 
                perms = {}
169
 
            if not isinstance(perms, list):
170
 
                perms = [perms]
171
 
 
172
 
            error = None
173
 
            for perm in perms:
174
 
                u, g = util.extract_usergroup(perm)
175
 
                try:
176
 
                    util.chownbyname(log_file, u, g)
177
 
                    return
178
 
                except OSError as e:
179
 
                    error = e
180
 
 
181
 
            LOG.warn("Failed changing perms on '%s'. tried: %s. %s",
182
 
                     log_file, ','.join(perms), error)
183
 
 
184
 
    def read_cfg(self, extra_fns=None):
185
 
        # None check so that we don't keep on re-loading if empty
186
 
        if self._cfg is None:
187
 
            self._cfg = self._read_cfg(extra_fns)
188
 
            # LOG.debug("Loaded 'init' config %s", self._cfg)
189
 
 
190
 
    def _read_cfg(self, extra_fns):
191
 
        no_cfg_paths = helpers.Paths({}, self.datasource)
192
 
        merger = helpers.ConfigMerger(paths=no_cfg_paths,
193
 
                                      datasource=self.datasource,
194
 
                                      additional_fns=extra_fns,
195
 
                                      base_cfg=fetch_base_config())
196
 
        return merger.cfg
197
 
 
198
 
    def _restore_from_cache(self):
199
 
        # We try to restore from a current link and static path
200
 
        # by using the instance link, if purge_cache was called
201
 
        # the file wont exist.
202
 
        return _pkl_load(self.paths.get_ipath_cur('obj_pkl'))
203
 
 
204
 
    def _write_to_cache(self):
205
 
        if self.datasource is NULL_DATA_SOURCE:
206
 
            return False
207
 
        return _pkl_store(self.datasource, self.paths.get_ipath_cur("obj_pkl"))
208
 
 
209
 
    def _get_datasources(self):
210
 
        # Any config provided???
211
 
        pkg_list = self.cfg.get('datasource_pkg_list') or []
212
 
        # Add the defaults at the end
213
 
        for n in ['', type_utils.obj_name(sources)]:
214
 
            if n not in pkg_list:
215
 
                pkg_list.append(n)
216
 
        cfg_list = self.cfg.get('datasource_list') or []
217
 
        return (cfg_list, pkg_list)
218
 
 
219
 
    def _restore_from_checked_cache(self, existing):
220
 
        if existing not in ("check", "trust"):
221
 
            raise ValueError("Unexpected value for existing: %s" % existing)
222
 
 
223
 
        ds = self._restore_from_cache()
224
 
        if not ds:
225
 
            return (None, "no cache found")
226
 
 
227
 
        run_iid_fn = self.paths.get_runpath('instance_id')
228
 
        if os.path.exists(run_iid_fn):
229
 
            run_iid = util.load_file(run_iid_fn).strip()
230
 
        else:
231
 
            run_iid = None
232
 
 
233
 
        if run_iid == ds.get_instance_id():
234
 
            return (ds, "restored from cache with run check: %s" % ds)
235
 
        elif existing == "trust":
236
 
            return (ds, "restored from cache: %s" % ds)
237
 
        else:
238
 
            if (hasattr(ds, 'check_instance_id') and
239
 
                    ds.check_instance_id(self.cfg)):
240
 
                return (ds, "restored from checked cache: %s" % ds)
241
 
            else:
242
 
                return (None, "cache invalid in datasource: %s" % ds)
243
 
 
244
 
    def _get_data_source(self, existing):
245
 
        if self.datasource is not NULL_DATA_SOURCE:
246
 
            return self.datasource
247
 
 
248
 
        with events.ReportEventStack(
249
 
                name="check-cache",
250
 
                description="attempting to read from cache [%s]" % existing,
251
 
                parent=self.reporter) as myrep:
252
 
 
253
 
            ds, desc = self._restore_from_checked_cache(existing)
254
 
            myrep.description = desc
255
 
            self.ds_restored = bool(ds)
256
 
            LOG.debug(myrep.description)
257
 
 
258
 
        if not ds:
259
 
            util.del_file(self.paths.instance_link)
260
 
            (cfg_list, pkg_list) = self._get_datasources()
261
 
            # Deep copy so that user-data handlers can not modify
262
 
            # (which will affect user-data handlers down the line...)
263
 
            (ds, dsname) = sources.find_source(self.cfg,
264
 
                                               self.distro,
265
 
                                               self.paths,
266
 
                                               copy.deepcopy(self.ds_deps),
267
 
                                               cfg_list,
268
 
                                               pkg_list, self.reporter)
269
 
            LOG.info("Loaded datasource %s - %s", dsname, ds)
270
 
        self.datasource = ds
271
 
        # Ensure we adjust our path members datasource
272
 
        # now that we have one (thus allowing ipath to be used)
273
 
        self._reset()
274
 
        return ds
275
 
 
276
 
    def _get_instance_subdirs(self):
277
 
        return ['handlers', 'scripts', 'sem']
278
 
 
279
 
    def _get_ipath(self, subname=None):
280
 
        # Force a check to see if anything
281
 
        # actually comes back, if not
282
 
        # then a datasource has not been assigned...
283
 
        instance_dir = self.paths.get_ipath(subname)
284
 
        if not instance_dir:
285
 
            raise RuntimeError(("No instance directory is available."
286
 
                                " Has a datasource been fetched??"))
287
 
        return instance_dir
288
 
 
289
 
    def _reflect_cur_instance(self):
290
 
        # Remove the old symlink and attach a new one so
291
 
        # that further reads/writes connect into the right location
292
 
        idir = self._get_ipath()
293
 
        util.del_file(self.paths.instance_link)
294
 
        util.sym_link(idir, self.paths.instance_link)
295
 
 
296
 
        # Ensures these dirs exist
297
 
        dir_list = []
298
 
        for d in self._get_instance_subdirs():
299
 
            dir_list.append(os.path.join(idir, d))
300
 
        util.ensure_dirs(dir_list)
301
 
 
302
 
        # Write out information on what is being used for the current instance
303
 
        # and what may have been used for a previous instance...
304
 
        dp = self.paths.get_cpath('data')
305
 
 
306
 
        # Write what the datasource was and is..
307
 
        ds = "%s: %s" % (type_utils.obj_name(self.datasource), self.datasource)
308
 
        previous_ds = None
309
 
        ds_fn = os.path.join(idir, 'datasource')
310
 
        try:
311
 
            previous_ds = util.load_file(ds_fn).strip()
312
 
        except Exception:
313
 
            pass
314
 
        if not previous_ds:
315
 
            previous_ds = ds
316
 
        util.write_file(ds_fn, "%s\n" % ds)
317
 
        util.write_file(os.path.join(dp, 'previous-datasource'),
318
 
                        "%s\n" % (previous_ds))
319
 
 
320
 
        # What the instance id was and is...
321
 
        iid = self.datasource.get_instance_id()
322
 
        iid_fn = os.path.join(dp, 'instance-id')
323
 
 
324
 
        previous_iid = self.previous_iid()
325
 
        util.write_file(iid_fn, "%s\n" % iid)
326
 
        util.write_file(self.paths.get_runpath('instance_id'), "%s\n" % iid)
327
 
        util.write_file(os.path.join(dp, 'previous-instance-id'),
328
 
                        "%s\n" % (previous_iid))
329
 
 
330
 
        self._write_to_cache()
331
 
        # Ensure needed components are regenerated
332
 
        # after change of instance which may cause
333
 
        # change of configuration
334
 
        self._reset()
335
 
        return iid
336
 
 
337
 
    def previous_iid(self):
338
 
        if self._previous_iid is not None:
339
 
            return self._previous_iid
340
 
 
341
 
        dp = self.paths.get_cpath('data')
342
 
        iid_fn = os.path.join(dp, 'instance-id')
343
 
        try:
344
 
            self._previous_iid = util.load_file(iid_fn).strip()
345
 
        except Exception:
346
 
            self._previous_iid = NO_PREVIOUS_INSTANCE_ID
347
 
 
348
 
        LOG.debug("previous iid found to be %s", self._previous_iid)
349
 
        return self._previous_iid
350
 
 
351
 
    def is_new_instance(self):
352
 
        previous = self.previous_iid()
353
 
        ret = (previous == NO_PREVIOUS_INSTANCE_ID or
354
 
               previous != self.datasource.get_instance_id())
355
 
        return ret
356
 
 
357
 
    def fetch(self, existing="check"):
358
 
        return self._get_data_source(existing=existing)
359
 
 
360
 
    def instancify(self):
361
 
        return self._reflect_cur_instance()
362
 
 
363
 
    def cloudify(self):
364
 
        # Form the needed options to cloudify our members
365
 
        return cloud.Cloud(self.datasource,
366
 
                           self.paths, self.cfg,
367
 
                           self.distro, helpers.Runners(self.paths),
368
 
                           reporter=self.reporter)
369
 
 
370
 
    def update(self):
371
 
        self._store_userdata()
372
 
        self._store_vendordata()
373
 
 
374
 
    def _store_userdata(self):
375
 
        raw_ud = self.datasource.get_userdata_raw()
376
 
        if raw_ud is None:
377
 
            raw_ud = b''
378
 
        util.write_file(self._get_ipath('userdata_raw'), raw_ud, 0o600)
379
 
        # processed userdata is a Mime message, so write it as string.
380
 
        processed_ud = self.datasource.get_userdata()
381
 
        if processed_ud is None:
382
 
            raw_ud = ''
383
 
        util.write_file(self._get_ipath('userdata'), str(processed_ud), 0o600)
384
 
 
385
 
    def _store_vendordata(self):
386
 
        raw_vd = self.datasource.get_vendordata_raw()
387
 
        if raw_vd is None:
388
 
            raw_vd = b''
389
 
        util.write_file(self._get_ipath('vendordata_raw'), raw_vd, 0o600)
390
 
        # processed vendor data is a Mime message, so write it as string.
391
 
        processed_vd = str(self.datasource.get_vendordata())
392
 
        if processed_vd is None:
393
 
            processed_vd = ''
394
 
        util.write_file(self._get_ipath('vendordata'), str(processed_vd),
395
 
                        0o600)
396
 
 
397
 
    def _default_handlers(self, opts=None):
398
 
        if opts is None:
399
 
            opts = {}
400
 
 
401
 
        opts.update({
402
 
            'paths': self.paths,
403
 
            'datasource': self.datasource,
404
 
        })
405
 
        # TODO(harlowja) Hmmm, should we dynamically import these??
406
 
        def_handlers = [
407
 
            cc_part.CloudConfigPartHandler(**opts),
408
 
            ss_part.ShellScriptPartHandler(**opts),
409
 
            bh_part.BootHookPartHandler(**opts),
410
 
            up_part.UpstartJobPartHandler(**opts),
411
 
        ]
412
 
        return def_handlers
413
 
 
414
 
    def _default_userdata_handlers(self):
415
 
        return self._default_handlers()
416
 
 
417
 
    def _default_vendordata_handlers(self):
418
 
        return self._default_handlers(
419
 
            opts={'script_path': 'vendor_scripts',
420
 
                  'cloud_config_path': 'vendor_cloud_config'})
421
 
 
422
 
    def _do_handlers(self, data_msg, c_handlers_list, frequency,
423
 
                     excluded=None):
424
 
        """
425
 
        Generalized handlers suitable for use with either vendordata
426
 
        or userdata
427
 
        """
428
 
        if excluded is None:
429
 
            excluded = []
430
 
 
431
 
        cdir = self.paths.get_cpath("handlers")
432
 
        idir = self._get_ipath("handlers")
433
 
 
434
 
        # Add the path to the plugins dir to the top of our list for importing
435
 
        # new handlers.
436
 
        #
437
 
        # Note(harlowja): instance dir should be read before cloud-dir
438
 
        for d in [cdir, idir]:
439
 
            if d and d not in sys.path:
440
 
                sys.path.insert(0, d)
441
 
 
442
 
        def register_handlers_in_dir(path):
443
 
            # Attempts to register any handler modules under the given path.
444
 
            if not path or not os.path.isdir(path):
445
 
                return
446
 
            potential_handlers = util.find_modules(path)
447
 
            for (fname, mod_name) in potential_handlers.items():
448
 
                try:
449
 
                    mod_locs, looked_locs = importer.find_module(
450
 
                        mod_name, [''], ['list_types', 'handle_part'])
451
 
                    if not mod_locs:
452
 
                        LOG.warn("Could not find a valid user-data handler"
453
 
                                 " named %s in file %s (searched %s)",
454
 
                                 mod_name, fname, looked_locs)
455
 
                        continue
456
 
                    mod = importer.import_module(mod_locs[0])
457
 
                    mod = handlers.fixup_handler(mod)
458
 
                    types = c_handlers.register(mod)
459
 
                    if types:
460
 
                        LOG.debug("Added custom handler for %s [%s] from %s",
461
 
                                  types, mod, fname)
462
 
                except Exception:
463
 
                    util.logexc(LOG, "Failed to register handler from %s",
464
 
                                fname)
465
 
 
466
 
        # This keeps track of all the active handlers
467
 
        c_handlers = helpers.ContentHandlers()
468
 
 
469
 
        # Add any handlers in the cloud-dir
470
 
        register_handlers_in_dir(cdir)
471
 
 
472
 
        # Register any other handlers that come from the default set. This
473
 
        # is done after the cloud-dir handlers so that the cdir modules can
474
 
        # take over the default user-data handler content-types.
475
 
        for mod in c_handlers_list:
476
 
            types = c_handlers.register(mod, overwrite=False)
477
 
            if types:
478
 
                LOG.debug("Added default handler for %s from %s", types, mod)
479
 
 
480
 
        # Form our cloud interface
481
 
        data = self.cloudify()
482
 
 
483
 
        def init_handlers():
484
 
            # Init the handlers first
485
 
            for (_ctype, mod) in c_handlers.items():
486
 
                if mod in c_handlers.initialized:
487
 
                    # Avoid initing the same module twice (if said module
488
 
                    # is registered to more than one content-type).
489
 
                    continue
490
 
                handlers.call_begin(mod, data, frequency)
491
 
                c_handlers.initialized.append(mod)
492
 
 
493
 
        def walk_handlers(excluded):
494
 
            # Walk the user data
495
 
            part_data = {
496
 
                'handlers': c_handlers,
497
 
                # Any new handlers that are encountered get writen here
498
 
                'handlerdir': idir,
499
 
                'data': data,
500
 
                # The default frequency if handlers don't have one
501
 
                'frequency': frequency,
502
 
                # This will be used when new handlers are found
503
 
                # to help write there contents to files with numbered
504
 
                # names...
505
 
                'handlercount': 0,
506
 
                'excluded': excluded,
507
 
            }
508
 
            handlers.walk(data_msg, handlers.walker_callback, data=part_data)
509
 
 
510
 
        def finalize_handlers():
511
 
            # Give callbacks opportunity to finalize
512
 
            for (_ctype, mod) in c_handlers.items():
513
 
                if mod not in c_handlers.initialized:
514
 
                    # Said module was never inited in the first place, so lets
515
 
                    # not attempt to finalize those that never got called.
516
 
                    continue
517
 
                c_handlers.initialized.remove(mod)
518
 
                try:
519
 
                    handlers.call_end(mod, data, frequency)
520
 
                except Exception:
521
 
                    util.logexc(LOG, "Failed to finalize handler: %s", mod)
522
 
 
523
 
        try:
524
 
            init_handlers()
525
 
            walk_handlers(excluded)
526
 
        finally:
527
 
            finalize_handlers()
528
 
 
529
 
    def consume_data(self, frequency=PER_INSTANCE):
530
 
        # Consume the userdata first, because we need want to let the part
531
 
        # handlers run first (for merging stuff)
532
 
        with events.ReportEventStack("consume-user-data",
533
 
                                     "reading and applying user-data",
534
 
                                     parent=self.reporter):
535
 
                self._consume_userdata(frequency)
536
 
        with events.ReportEventStack("consume-vendor-data",
537
 
                                     "reading and applying vendor-data",
538
 
                                     parent=self.reporter):
539
 
                self._consume_vendordata(frequency)
540
 
 
541
 
        # Perform post-consumption adjustments so that
542
 
        # modules that run during the init stage reflect
543
 
        # this consumed set.
544
 
        #
545
 
        # They will be recreated on future access...
546
 
        self._reset()
547
 
        # Note(harlowja): the 'active' datasource will have
548
 
        # references to the previous config, distro, paths
549
 
        # objects before the load of the userdata happened,
550
 
        # this is expected.
551
 
 
552
 
    def _consume_vendordata(self, frequency=PER_INSTANCE):
553
 
        """
554
 
        Consume the vendordata and run the part handlers on it
555
 
        """
556
 
        # User-data should have been consumed first.
557
 
        # So we merge the other available cloud-configs (everything except
558
 
        # vendor provided), and check whether or not we should consume
559
 
        # vendor data at all. That gives user or system a chance to override.
560
 
        if not self.datasource.get_vendordata_raw():
561
 
            LOG.debug("no vendordata from datasource")
562
 
            return
563
 
 
564
 
        _cc_merger = helpers.ConfigMerger(paths=self._paths,
565
 
                                          datasource=self.datasource,
566
 
                                          additional_fns=[],
567
 
                                          base_cfg=self.cfg,
568
 
                                          include_vendor=False)
569
 
        vdcfg = _cc_merger.cfg.get('vendor_data', {})
570
 
 
571
 
        if not isinstance(vdcfg, dict):
572
 
            vdcfg = {'enabled': False}
573
 
            LOG.warn("invalid 'vendor_data' setting. resetting to: %s", vdcfg)
574
 
 
575
 
        enabled = vdcfg.get('enabled')
576
 
        no_handlers = vdcfg.get('disabled_handlers', None)
577
 
 
578
 
        if not util.is_true(enabled):
579
 
            LOG.debug("vendordata consumption is disabled.")
580
 
            return
581
 
 
582
 
        LOG.debug("vendor data will be consumed. disabled_handlers=%s",
583
 
                  no_handlers)
584
 
 
585
 
        # Ensure vendordata source fetched before activation (just incase)
586
 
        vendor_data_msg = self.datasource.get_vendordata()
587
 
 
588
 
        # This keeps track of all the active handlers, while excluding what the
589
 
        # users doesn't want run, i.e. boot_hook, cloud_config, shell_script
590
 
        c_handlers_list = self._default_vendordata_handlers()
591
 
 
592
 
        # Run the handlers
593
 
        self._do_handlers(vendor_data_msg, c_handlers_list, frequency,
594
 
                          excluded=no_handlers)
595
 
 
596
 
    def _consume_userdata(self, frequency=PER_INSTANCE):
597
 
        """
598
 
        Consume the userdata and run the part handlers
599
 
        """
600
 
 
601
 
        # Ensure datasource fetched before activation (just incase)
602
 
        user_data_msg = self.datasource.get_userdata(True)
603
 
 
604
 
        # This keeps track of all the active handlers
605
 
        c_handlers_list = self._default_handlers()
606
 
 
607
 
        # Run the handlers
608
 
        self._do_handlers(user_data_msg, c_handlers_list, frequency)
609
 
 
610
 
    def _find_networking_config(self):
611
 
        disable_file = os.path.join(
612
 
            self.paths.get_cpath('data'), 'upgraded-network')
613
 
        if os.path.exists(disable_file):
614
 
            return (None, disable_file)
615
 
 
616
 
        cmdline_cfg = ('cmdline', cmdline.read_kernel_cmdline_config())
617
 
        dscfg = ('ds', None)
618
 
        if self.datasource and hasattr(self.datasource, 'network_config'):
619
 
            dscfg = ('ds', self.datasource.network_config)
620
 
        sys_cfg = ('system_cfg', self.cfg.get('network'))
621
 
 
622
 
        for loc, ncfg in (cmdline_cfg, sys_cfg, dscfg):
623
 
            if net.is_disabled_cfg(ncfg):
624
 
                LOG.debug("network config disabled by %s", loc)
625
 
                return (None, loc)
626
 
            if ncfg:
627
 
                return (ncfg, loc)
628
 
        return (net.generate_fallback_config(), "fallback")
629
 
 
630
 
    def apply_network_config(self, bring_up):
631
 
        netcfg, src = self._find_networking_config()
632
 
        if netcfg is None:
633
 
            LOG.info("network config is disabled by %s", src)
634
 
            return
635
 
 
636
 
        try:
637
 
            LOG.debug("applying net config names for %s" % netcfg)
638
 
            self.distro.apply_network_config_names(netcfg)
639
 
        except Exception as e:
640
 
            LOG.warn("Failed to rename devices: %s", e)
641
 
 
642
 
        if (self.datasource is not NULL_DATA_SOURCE and
643
 
                not self.is_new_instance()):
644
 
            LOG.debug("not a new instance. network config is not applied.")
645
 
            return
646
 
 
647
 
        LOG.info("Applying network configuration from %s bringup=%s: %s",
648
 
                 src, bring_up, netcfg)
649
 
        try:
650
 
            return self.distro.apply_network_config(netcfg, bring_up=bring_up)
651
 
        except NotImplementedError:
652
 
            LOG.warn("distro '%s' does not implement apply_network_config. "
653
 
                     "networking may not be configured properly." %
654
 
                     self.distro)
655
 
            return
656
 
 
657
 
 
658
 
class Modules(object):
659
 
    def __init__(self, init, cfg_files=None, reporter=None):
660
 
        self.init = init
661
 
        self.cfg_files = cfg_files
662
 
        # Created on first use
663
 
        self._cached_cfg = None
664
 
        if reporter is None:
665
 
            reporter = events.ReportEventStack(
666
 
                name="module-reporter", description="module-desc",
667
 
                reporting_enabled=False)
668
 
        self.reporter = reporter
669
 
 
670
 
    @property
671
 
    def cfg(self):
672
 
        # None check to avoid empty case causing re-reading
673
 
        if self._cached_cfg is None:
674
 
            merger = helpers.ConfigMerger(paths=self.init.paths,
675
 
                                          datasource=self.init.datasource,
676
 
                                          additional_fns=self.cfg_files,
677
 
                                          base_cfg=self.init.cfg)
678
 
            self._cached_cfg = merger.cfg
679
 
            # LOG.debug("Loading 'module' config %s", self._cached_cfg)
680
 
        # Only give out a copy so that others can't modify this...
681
 
        return copy.deepcopy(self._cached_cfg)
682
 
 
683
 
    def _read_modules(self, name):
684
 
        module_list = []
685
 
        if name not in self.cfg:
686
 
            return module_list
687
 
        cfg_mods = self.cfg[name]
688
 
        # Create 'module_list', an array of hashes
689
 
        # Where hash['mod'] = module name
690
 
        #       hash['freq'] = frequency
691
 
        #       hash['args'] = arguments
692
 
        for item in cfg_mods:
693
 
            if not item:
694
 
                continue
695
 
            if isinstance(item, six.string_types):
696
 
                module_list.append({
697
 
                    'mod': item.strip(),
698
 
                })
699
 
            elif isinstance(item, (list)):
700
 
                contents = {}
701
 
                # Meant to fall through...
702
 
                if len(item) >= 1:
703
 
                    contents['mod'] = item[0].strip()
704
 
                if len(item) >= 2:
705
 
                    contents['freq'] = item[1].strip()
706
 
                if len(item) >= 3:
707
 
                    contents['args'] = item[2:]
708
 
                if contents:
709
 
                    module_list.append(contents)
710
 
            elif isinstance(item, (dict)):
711
 
                contents = {}
712
 
                valid = False
713
 
                if 'name' in item:
714
 
                    contents['mod'] = item['name'].strip()
715
 
                    valid = True
716
 
                if 'frequency' in item:
717
 
                    contents['freq'] = item['frequency'].strip()
718
 
                if 'args' in item:
719
 
                    contents['args'] = item['args'] or []
720
 
                if contents and valid:
721
 
                    module_list.append(contents)
722
 
            else:
723
 
                raise TypeError(("Failed to read '%s' item in config,"
724
 
                                 " unknown type %s") %
725
 
                                (item, type_utils.obj_name(item)))
726
 
        return module_list
727
 
 
728
 
    def _fixup_modules(self, raw_mods):
729
 
        mostly_mods = []
730
 
        for raw_mod in raw_mods:
731
 
            raw_name = raw_mod['mod']
732
 
            freq = raw_mod.get('freq')
733
 
            run_args = raw_mod.get('args') or []
734
 
            mod_name = config.form_module_name(raw_name)
735
 
            if not mod_name:
736
 
                continue
737
 
            if freq and freq not in FREQUENCIES:
738
 
                LOG.warn(("Config specified module %s"
739
 
                          " has an unknown frequency %s"), raw_name, freq)
740
 
                # Reset it so when ran it will get set to a known value
741
 
                freq = None
742
 
            mod_locs, looked_locs = importer.find_module(
743
 
                mod_name, ['', type_utils.obj_name(config)], ['handle'])
744
 
            if not mod_locs:
745
 
                LOG.warn("Could not find module named %s (searched %s)",
746
 
                         mod_name, looked_locs)
747
 
                continue
748
 
            mod = config.fixup_module(importer.import_module(mod_locs[0]))
749
 
            mostly_mods.append([mod, raw_name, freq, run_args])
750
 
        return mostly_mods
751
 
 
752
 
    def _run_modules(self, mostly_mods):
753
 
        cc = self.init.cloudify()
754
 
        # Return which ones ran
755
 
        # and which ones failed + the exception of why it failed
756
 
        failures = []
757
 
        which_ran = []
758
 
        for (mod, name, freq, args) in mostly_mods:
759
 
            try:
760
 
                # Try the modules frequency, otherwise fallback to a known one
761
 
                if not freq:
762
 
                    freq = mod.frequency
763
 
                if freq not in FREQUENCIES:
764
 
                    freq = PER_INSTANCE
765
 
                LOG.debug("Running module %s (%s) with frequency %s",
766
 
                          name, mod, freq)
767
 
 
768
 
                # Use the configs logger and not our own
769
 
                # TODO(harlowja): possibly check the module
770
 
                # for having a LOG attr and just give it back
771
 
                # its own logger?
772
 
                func_args = [name, self.cfg,
773
 
                             cc, config.LOG, args]
774
 
                # Mark it as having started running
775
 
                which_ran.append(name)
776
 
                # This name will affect the semaphore name created
777
 
                run_name = "config-%s" % (name)
778
 
 
779
 
                desc = "running %s with frequency %s" % (run_name, freq)
780
 
                myrep = events.ReportEventStack(
781
 
                    name=run_name, description=desc, parent=self.reporter)
782
 
 
783
 
                with myrep:
784
 
                    ran, _r = cc.run(run_name, mod.handle, func_args,
785
 
                                     freq=freq)
786
 
                    if ran:
787
 
                        myrep.message = "%s ran successfully" % run_name
788
 
                    else:
789
 
                        myrep.message = "%s previously ran" % run_name
790
 
 
791
 
            except Exception as e:
792
 
                util.logexc(LOG, "Running module %s (%s) failed", name, mod)
793
 
                failures.append((name, e))
794
 
        return (which_ran, failures)
795
 
 
796
 
    def run_single(self, mod_name, args=None, freq=None):
797
 
        # Form the users module 'specs'
798
 
        mod_to_be = {
799
 
            'mod': mod_name,
800
 
            'args': args,
801
 
            'freq': freq,
802
 
        }
803
 
        # Now resume doing the normal fixups and running
804
 
        raw_mods = [mod_to_be]
805
 
        mostly_mods = self._fixup_modules(raw_mods)
806
 
        return self._run_modules(mostly_mods)
807
 
 
808
 
    def run_section(self, section_name):
809
 
        raw_mods = self._read_modules(section_name)
810
 
        mostly_mods = self._fixup_modules(raw_mods)
811
 
        d_name = self.init.distro.name
812
 
 
813
 
        skipped = []
814
 
        forced = []
815
 
        overridden = self.cfg.get('unverified_modules', [])
816
 
        for (mod, name, _freq, _args) in mostly_mods:
817
 
            worked_distros = set(mod.distros)
818
 
            worked_distros.update(
819
 
                distros.Distro.expand_osfamily(mod.osfamilies))
820
 
 
821
 
            # module does not declare 'distros' or lists this distro
822
 
            if not worked_distros or d_name in worked_distros:
823
 
                continue
824
 
 
825
 
            if name in overridden:
826
 
                forced.append(name)
827
 
            else:
828
 
                skipped.append(name)
829
 
 
830
 
        if skipped:
831
 
            LOG.info("Skipping modules %s because they are not verified "
832
 
                     "on distro '%s'.  To run anyway, add them to "
833
 
                     "'unverified_modules' in config.", skipped, d_name)
834
 
        if forced:
835
 
            LOG.info("running unverified_modules: %s", forced)
836
 
 
837
 
        return self._run_modules(mostly_mods)
838
 
 
839
 
 
840
 
def fetch_base_config():
841
 
    base_cfgs = []
842
 
    default_cfg = util.get_builtin_cfg()
843
 
 
844
 
    # Anything in your conf.d location??
845
 
    # or the 'default' cloud.cfg location???
846
 
    base_cfgs.append(util.read_conf_with_confd(CLOUD_CONFIG))
847
 
 
848
 
    # Kernel/cmdline parameters override system config
849
 
    kern_contents = util.read_cc_from_cmdline()
850
 
    if kern_contents:
851
 
        base_cfgs.append(util.load_yaml(kern_contents, default={}))
852
 
 
853
 
    # And finally the default gets to play
854
 
    if default_cfg:
855
 
        base_cfgs.append(default_cfg)
856
 
 
857
 
    return util.mergemanydict(base_cfgs)
858
 
 
859
 
 
860
 
def _pkl_store(obj, fname):
861
 
    try:
862
 
        pk_contents = pickle.dumps(obj)
863
 
    except Exception:
864
 
        util.logexc(LOG, "Failed pickling datasource %s", obj)
865
 
        return False
866
 
    try:
867
 
        util.write_file(fname, pk_contents, omode="wb", mode=0o400)
868
 
    except Exception:
869
 
        util.logexc(LOG, "Failed pickling datasource to %s", fname)
870
 
        return False
871
 
    return True
872
 
 
873
 
 
874
 
def _pkl_load(fname):
875
 
    pickle_contents = None
876
 
    try:
877
 
        pickle_contents = util.load_file(fname, decode=False)
878
 
    except Exception as e:
879
 
        if os.path.isfile(fname):
880
 
            LOG.warn("failed loading pickle in %s: %s" % (fname, e))
881
 
        pass
882
 
 
883
 
    # This is allowed so just return nothing successfully loaded...
884
 
    if not pickle_contents:
885
 
        return None
886
 
    try:
887
 
        return pickle.loads(pickle_contents)
888
 
    except Exception:
889
 
        util.logexc(LOG, "Failed loading pickled blob from %s", fname)
890
 
        return None