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

« back to all changes in this revision

Viewing changes to cloudinit/helpers.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
 
from time import time
24
 
 
25
 
import contextlib
26
 
import os
27
 
 
28
 
import six
29
 
from six.moves.configparser import (
30
 
    NoSectionError, NoOptionError, RawConfigParser)
31
 
 
32
 
from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
33
 
                                CFG_ENV_NAME)
34
 
 
35
 
from cloudinit import log as logging
36
 
from cloudinit import type_utils
37
 
from cloudinit import util
38
 
 
39
 
LOG = logging.getLogger(__name__)
40
 
 
41
 
 
42
 
class LockFailure(Exception):
43
 
    pass
44
 
 
45
 
 
46
 
class DummyLock(object):
47
 
    pass
48
 
 
49
 
 
50
 
class DummySemaphores(object):
51
 
    def __init__(self):
52
 
        pass
53
 
 
54
 
    @contextlib.contextmanager
55
 
    def lock(self, _name, _freq, _clear_on_fail=False):
56
 
        yield DummyLock()
57
 
 
58
 
    def has_run(self, _name, _freq):
59
 
        return False
60
 
 
61
 
    def clear(self, _name, _freq):
62
 
        return True
63
 
 
64
 
    def clear_all(self):
65
 
        pass
66
 
 
67
 
 
68
 
class FileLock(object):
69
 
    def __init__(self, fn):
70
 
        self.fn = fn
71
 
 
72
 
    def __str__(self):
73
 
        return "<%s using file %r>" % (type_utils.obj_name(self), self.fn)
74
 
 
75
 
 
76
 
def canon_sem_name(name):
77
 
    return name.replace("-", "_")
78
 
 
79
 
 
80
 
class FileSemaphores(object):
81
 
    def __init__(self, sem_path):
82
 
        self.sem_path = sem_path
83
 
 
84
 
    @contextlib.contextmanager
85
 
    def lock(self, name, freq, clear_on_fail=False):
86
 
        name = canon_sem_name(name)
87
 
        try:
88
 
            yield self._acquire(name, freq)
89
 
        except Exception:
90
 
            if clear_on_fail:
91
 
                self.clear(name, freq)
92
 
            raise
93
 
 
94
 
    def clear(self, name, freq):
95
 
        name = canon_sem_name(name)
96
 
        sem_file = self._get_path(name, freq)
97
 
        try:
98
 
            util.del_file(sem_file)
99
 
        except (IOError, OSError):
100
 
            util.logexc(LOG, "Failed deleting semaphore %s", sem_file)
101
 
            return False
102
 
        return True
103
 
 
104
 
    def clear_all(self):
105
 
        try:
106
 
            util.del_dir(self.sem_path)
107
 
        except (IOError, OSError):
108
 
            util.logexc(LOG, "Failed deleting semaphore directory %s",
109
 
                        self.sem_path)
110
 
 
111
 
    def _acquire(self, name, freq):
112
 
        # Check again if its been already gotten
113
 
        if self.has_run(name, freq):
114
 
            return None
115
 
        # This is a race condition since nothing atomic is happening
116
 
        # here, but this should be ok due to the nature of when
117
 
        # and where cloud-init runs... (file writing is not a lock...)
118
 
        sem_file = self._get_path(name, freq)
119
 
        contents = "%s: %s\n" % (os.getpid(), time())
120
 
        try:
121
 
            util.write_file(sem_file, contents)
122
 
        except (IOError, OSError):
123
 
            util.logexc(LOG, "Failed writing semaphore file %s", sem_file)
124
 
            return None
125
 
        return FileLock(sem_file)
126
 
 
127
 
    def has_run(self, name, freq):
128
 
        if not freq or freq == PER_ALWAYS:
129
 
            return False
130
 
 
131
 
        cname = canon_sem_name(name)
132
 
        sem_file = self._get_path(cname, freq)
133
 
        # This isn't really a good atomic check
134
 
        # but it suffices for where and when cloudinit runs
135
 
        if os.path.exists(sem_file):
136
 
            return True
137
 
 
138
 
        # this case could happen if the migrator module hadn't run yet
139
 
        # but the item had run before we did canon_sem_name.
140
 
        if cname != name and os.path.exists(self._get_path(name, freq)):
141
 
            LOG.warn("%s has run without canonicalized name [%s].\n"
142
 
                     "likely the migrator has not yet run. "
143
 
                     "It will run next boot.\n"
144
 
                     "run manually with: cloud-init single --name=migrator"
145
 
                     % (name, cname))
146
 
            return True
147
 
 
148
 
        return False
149
 
 
150
 
    def _get_path(self, name, freq):
151
 
        sem_path = self.sem_path
152
 
        if not freq or freq == PER_INSTANCE:
153
 
            return os.path.join(sem_path, name)
154
 
        else:
155
 
            return os.path.join(sem_path, "%s.%s" % (name, freq))
156
 
 
157
 
 
158
 
class Runners(object):
159
 
    def __init__(self, paths):
160
 
        self.paths = paths
161
 
        self.sems = {}
162
 
 
163
 
    def _get_sem(self, freq):
164
 
        if freq == PER_ALWAYS or not freq:
165
 
            return None
166
 
        sem_path = None
167
 
        if freq == PER_INSTANCE:
168
 
            # This may not exist,
169
 
            # so thats why we still check for none
170
 
            # below if say the paths object
171
 
            # doesn't have a datasource that can
172
 
            # provide this instance path...
173
 
            sem_path = self.paths.get_ipath("sem")
174
 
        elif freq == PER_ONCE:
175
 
            sem_path = self.paths.get_cpath("sem")
176
 
        if not sem_path:
177
 
            return None
178
 
        if sem_path not in self.sems:
179
 
            self.sems[sem_path] = FileSemaphores(sem_path)
180
 
        return self.sems[sem_path]
181
 
 
182
 
    def run(self, name, functor, args, freq=None, clear_on_fail=False):
183
 
        sem = self._get_sem(freq)
184
 
        if not sem:
185
 
            sem = DummySemaphores()
186
 
        if not args:
187
 
            args = []
188
 
        if sem.has_run(name, freq):
189
 
            LOG.debug("%s already ran (freq=%s)", name, freq)
190
 
            return (False, None)
191
 
        with sem.lock(name, freq, clear_on_fail) as lk:
192
 
            if not lk:
193
 
                raise LockFailure("Failed to acquire lock for %s" % name)
194
 
            else:
195
 
                LOG.debug("Running %s using lock (%s)", name, lk)
196
 
                if isinstance(args, (dict)):
197
 
                    results = functor(**args)
198
 
                else:
199
 
                    results = functor(*args)
200
 
                return (True, results)
201
 
 
202
 
 
203
 
class ConfigMerger(object):
204
 
    def __init__(self, paths=None, datasource=None,
205
 
                 additional_fns=None, base_cfg=None,
206
 
                 include_vendor=True):
207
 
        self._paths = paths
208
 
        self._ds = datasource
209
 
        self._fns = additional_fns
210
 
        self._base_cfg = base_cfg
211
 
        self._include_vendor = include_vendor
212
 
        # Created on first use
213
 
        self._cfg = None
214
 
 
215
 
    def _get_datasource_configs(self):
216
 
        d_cfgs = []
217
 
        if self._ds:
218
 
            try:
219
 
                ds_cfg = self._ds.get_config_obj()
220
 
                if ds_cfg and isinstance(ds_cfg, (dict)):
221
 
                    d_cfgs.append(ds_cfg)
222
 
            except Exception:
223
 
                util.logexc(LOG, "Failed loading of datasource config object "
224
 
                            "from %s", self._ds)
225
 
        return d_cfgs
226
 
 
227
 
    def _get_env_configs(self):
228
 
        e_cfgs = []
229
 
        if CFG_ENV_NAME in os.environ:
230
 
            e_fn = os.environ[CFG_ENV_NAME]
231
 
            try:
232
 
                e_cfgs.append(util.read_conf(e_fn))
233
 
            except Exception:
234
 
                util.logexc(LOG, 'Failed loading of env. config from %s',
235
 
                            e_fn)
236
 
        return e_cfgs
237
 
 
238
 
    def _get_instance_configs(self):
239
 
        i_cfgs = []
240
 
        # If cloud-config was written, pick it up as
241
 
        # a configuration file to use when running...
242
 
        if not self._paths:
243
 
            return i_cfgs
244
 
 
245
 
        cc_paths = ['cloud_config']
246
 
        if self._include_vendor:
247
 
            cc_paths.append('vendor_cloud_config')
248
 
 
249
 
        for cc_p in cc_paths:
250
 
            cc_fn = self._paths.get_ipath_cur(cc_p)
251
 
            if cc_fn and os.path.isfile(cc_fn):
252
 
                try:
253
 
                    i_cfgs.append(util.read_conf(cc_fn))
254
 
                except Exception:
255
 
                    util.logexc(LOG, 'Failed loading of cloud-config from %s',
256
 
                                cc_fn)
257
 
        return i_cfgs
258
 
 
259
 
    def _read_cfg(self):
260
 
        # Input config files override
261
 
        # env config files which
262
 
        # override instance configs
263
 
        # which override datasource
264
 
        # configs which override
265
 
        # base configuration
266
 
        cfgs = []
267
 
        if self._fns:
268
 
            for c_fn in self._fns:
269
 
                try:
270
 
                    cfgs.append(util.read_conf(c_fn))
271
 
                except Exception:
272
 
                    util.logexc(LOG, "Failed loading of configuration from %s",
273
 
                                c_fn)
274
 
 
275
 
        cfgs.extend(self._get_env_configs())
276
 
        cfgs.extend(self._get_instance_configs())
277
 
        cfgs.extend(self._get_datasource_configs())
278
 
        if self._base_cfg:
279
 
            cfgs.append(self._base_cfg)
280
 
        return util.mergemanydict(cfgs)
281
 
 
282
 
    @property
283
 
    def cfg(self):
284
 
        # None check to avoid empty case causing re-reading
285
 
        if self._cfg is None:
286
 
            self._cfg = self._read_cfg()
287
 
        return self._cfg
288
 
 
289
 
 
290
 
class ContentHandlers(object):
291
 
 
292
 
    def __init__(self):
293
 
        self.registered = {}
294
 
        self.initialized = []
295
 
 
296
 
    def __contains__(self, item):
297
 
        return self.is_registered(item)
298
 
 
299
 
    def __getitem__(self, key):
300
 
        return self._get_handler(key)
301
 
 
302
 
    def is_registered(self, content_type):
303
 
        return content_type in self.registered
304
 
 
305
 
    def register(self, mod, initialized=False, overwrite=True):
306
 
        types = set()
307
 
        for t in mod.list_types():
308
 
            if overwrite:
309
 
                types.add(t)
310
 
            else:
311
 
                if not self.is_registered(t):
312
 
                    types.add(t)
313
 
        for t in types:
314
 
            self.registered[t] = mod
315
 
        if initialized and mod not in self.initialized:
316
 
            self.initialized.append(mod)
317
 
        return types
318
 
 
319
 
    def _get_handler(self, content_type):
320
 
        return self.registered[content_type]
321
 
 
322
 
    def items(self):
323
 
        return list(self.registered.items())
324
 
 
325
 
 
326
 
class Paths(object):
327
 
    def __init__(self, path_cfgs, ds=None):
328
 
        self.cfgs = path_cfgs
329
 
        # Populate all the initial paths
330
 
        self.cloud_dir = path_cfgs.get('cloud_dir', '/var/lib/cloud')
331
 
        self.run_dir = path_cfgs.get('run_dir', '/run/cloud-init')
332
 
        self.instance_link = os.path.join(self.cloud_dir, 'instance')
333
 
        self.boot_finished = os.path.join(self.instance_link, "boot-finished")
334
 
        self.upstart_conf_d = path_cfgs.get('upstart_dir')
335
 
        self.seed_dir = os.path.join(self.cloud_dir, 'seed')
336
 
        # This one isn't joined, since it should just be read-only
337
 
        template_dir = path_cfgs.get('templates_dir', '/etc/cloud/templates/')
338
 
        self.template_tpl = os.path.join(template_dir, '%s.tmpl')
339
 
        self.lookups = {
340
 
            "handlers": "handlers",
341
 
            "scripts": "scripts",
342
 
            "vendor_scripts": "scripts/vendor",
343
 
            "sem": "sem",
344
 
            "boothooks": "boothooks",
345
 
            "userdata_raw": "user-data.txt",
346
 
            "userdata": "user-data.txt.i",
347
 
            "obj_pkl": "obj.pkl",
348
 
            "cloud_config": "cloud-config.txt",
349
 
            "vendor_cloud_config": "vendor-cloud-config.txt",
350
 
            "data": "data",
351
 
            "vendordata_raw": "vendor-data.txt",
352
 
            "vendordata": "vendor-data.txt.i",
353
 
            "instance_id": ".instance-id",
354
 
        }
355
 
        # Set when a datasource becomes active
356
 
        self.datasource = ds
357
 
 
358
 
    # get_ipath_cur: get the current instance path for an item
359
 
    def get_ipath_cur(self, name=None):
360
 
        return self._get_path(self.instance_link, name)
361
 
 
362
 
    # get_cpath : get the "clouddir" (/var/lib/cloud/<name>)
363
 
    # for a name in dirmap
364
 
    def get_cpath(self, name=None):
365
 
        return self._get_path(self.cloud_dir, name)
366
 
 
367
 
    # _get_ipath : get the instance path for a name in pathmap
368
 
    # (/var/lib/cloud/instances/<instance>/<name>)
369
 
    def _get_ipath(self, name=None):
370
 
        if not self.datasource:
371
 
            return None
372
 
        iid = self.datasource.get_instance_id()
373
 
        if iid is None:
374
 
            return None
375
 
        path_safe_iid = str(iid).replace(os.sep, '_')
376
 
        ipath = os.path.join(self.cloud_dir, 'instances', path_safe_iid)
377
 
        add_on = self.lookups.get(name)
378
 
        if add_on:
379
 
            ipath = os.path.join(ipath, add_on)
380
 
        return ipath
381
 
 
382
 
    # get_ipath : get the instance path for a name in pathmap
383
 
    # (/var/lib/cloud/instances/<instance>/<name>)
384
 
    # returns None + warns if no active datasource....
385
 
    def get_ipath(self, name=None):
386
 
        ipath = self._get_ipath(name)
387
 
        if not ipath:
388
 
            LOG.warn(("No per instance data available, "
389
 
                      "is there an datasource/iid set?"))
390
 
            return None
391
 
        else:
392
 
            return ipath
393
 
 
394
 
    def _get_path(self, base, name=None):
395
 
        if name is None:
396
 
            return base
397
 
        return os.path.join(base, self.lookups[name])
398
 
 
399
 
    def get_runpath(self, name=None):
400
 
        return self._get_path(self.run_dir, name)
401
 
 
402
 
 
403
 
# This config parser will not throw when sections don't exist
404
 
# and you are setting values on those sections which is useful
405
 
# when writing to new options that may not have corresponding
406
 
# sections. Also it can default other values when doing gets
407
 
# so that if those sections/options do not exist you will
408
 
# get a default instead of an error. Another useful case where
409
 
# you can avoid catching exceptions that you typically don't
410
 
# care about...
411
 
 
412
 
class DefaultingConfigParser(RawConfigParser):
413
 
    DEF_INT = 0
414
 
    DEF_FLOAT = 0.0
415
 
    DEF_BOOLEAN = False
416
 
    DEF_BASE = None
417
 
 
418
 
    def get(self, section, option):
419
 
        value = self.DEF_BASE
420
 
        try:
421
 
            value = RawConfigParser.get(self, section, option)
422
 
        except NoSectionError:
423
 
            pass
424
 
        except NoOptionError:
425
 
            pass
426
 
        return value
427
 
 
428
 
    def set(self, section, option, value=None):
429
 
        if not self.has_section(section) and section.lower() != 'default':
430
 
            self.add_section(section)
431
 
        RawConfigParser.set(self, section, option, value)
432
 
 
433
 
    def remove_option(self, section, option):
434
 
        if self.has_option(section, option):
435
 
            RawConfigParser.remove_option(self, section, option)
436
 
 
437
 
    def getboolean(self, section, option):
438
 
        if not self.has_option(section, option):
439
 
            return self.DEF_BOOLEAN
440
 
        return RawConfigParser.getboolean(self, section, option)
441
 
 
442
 
    def getfloat(self, section, option):
443
 
        if not self.has_option(section, option):
444
 
            return self.DEF_FLOAT
445
 
        return RawConfigParser.getfloat(self, section, option)
446
 
 
447
 
    def getint(self, section, option):
448
 
        if not self.has_option(section, option):
449
 
            return self.DEF_INT
450
 
        return RawConfigParser.getint(self, section, option)
451
 
 
452
 
    def stringify(self, header=None):
453
 
        contents = ''
454
 
        with six.StringIO() as outputstream:
455
 
            self.write(outputstream)
456
 
            outputstream.flush()
457
 
            contents = outputstream.getvalue()
458
 
            if header:
459
 
                contents = "\n".join([header, contents])
460
 
        return contents