3
# Copyright (C) 2012 Canonical Ltd.
4
# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
5
# Copyright (C) 2012 Yahoo! Inc.
7
# Author: Scott Moser <scott.moser@canonical.com>
8
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
9
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
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.
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.
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/>.
29
from six.moves.configparser import (
30
NoSectionError, NoOptionError, RawConfigParser)
32
from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
35
from cloudinit import log as logging
36
from cloudinit import type_utils
37
from cloudinit import util
39
LOG = logging.getLogger(__name__)
42
class LockFailure(Exception):
46
class DummyLock(object):
50
class DummySemaphores(object):
54
@contextlib.contextmanager
55
def lock(self, _name, _freq, _clear_on_fail=False):
58
def has_run(self, _name, _freq):
61
def clear(self, _name, _freq):
68
class FileLock(object):
69
def __init__(self, fn):
73
return "<%s using file %r>" % (type_utils.obj_name(self), self.fn)
76
def canon_sem_name(name):
77
return name.replace("-", "_")
80
class FileSemaphores(object):
81
def __init__(self, sem_path):
82
self.sem_path = sem_path
84
@contextlib.contextmanager
85
def lock(self, name, freq, clear_on_fail=False):
86
name = canon_sem_name(name)
88
yield self._acquire(name, freq)
91
self.clear(name, freq)
94
def clear(self, name, freq):
95
name = canon_sem_name(name)
96
sem_file = self._get_path(name, freq)
98
util.del_file(sem_file)
99
except (IOError, OSError):
100
util.logexc(LOG, "Failed deleting semaphore %s", sem_file)
106
util.del_dir(self.sem_path)
107
except (IOError, OSError):
108
util.logexc(LOG, "Failed deleting semaphore directory %s",
111
def _acquire(self, name, freq):
112
# Check again if its been already gotten
113
if self.has_run(name, freq):
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())
121
util.write_file(sem_file, contents)
122
except (IOError, OSError):
123
util.logexc(LOG, "Failed writing semaphore file %s", sem_file)
125
return FileLock(sem_file)
127
def has_run(self, name, freq):
128
if not freq or freq == PER_ALWAYS:
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):
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"
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)
155
return os.path.join(sem_path, "%s.%s" % (name, freq))
158
class Runners(object):
159
def __init__(self, paths):
163
def _get_sem(self, freq):
164
if freq == PER_ALWAYS or not freq:
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")
178
if sem_path not in self.sems:
179
self.sems[sem_path] = FileSemaphores(sem_path)
180
return self.sems[sem_path]
182
def run(self, name, functor, args, freq=None, clear_on_fail=False):
183
sem = self._get_sem(freq)
185
sem = DummySemaphores()
188
if sem.has_run(name, freq):
189
LOG.debug("%s already ran (freq=%s)", name, freq)
191
with sem.lock(name, freq, clear_on_fail) as lk:
193
raise LockFailure("Failed to acquire lock for %s" % name)
195
LOG.debug("Running %s using lock (%s)", name, lk)
196
if isinstance(args, (dict)):
197
results = functor(**args)
199
results = functor(*args)
200
return (True, results)
203
class ConfigMerger(object):
204
def __init__(self, paths=None, datasource=None,
205
additional_fns=None, base_cfg=None,
206
include_vendor=True):
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
215
def _get_datasource_configs(self):
219
ds_cfg = self._ds.get_config_obj()
220
if ds_cfg and isinstance(ds_cfg, (dict)):
221
d_cfgs.append(ds_cfg)
223
util.logexc(LOG, "Failed loading of datasource config object "
227
def _get_env_configs(self):
229
if CFG_ENV_NAME in os.environ:
230
e_fn = os.environ[CFG_ENV_NAME]
232
e_cfgs.append(util.read_conf(e_fn))
234
util.logexc(LOG, 'Failed loading of env. config from %s',
238
def _get_instance_configs(self):
240
# If cloud-config was written, pick it up as
241
# a configuration file to use when running...
245
cc_paths = ['cloud_config']
246
if self._include_vendor:
247
cc_paths.append('vendor_cloud_config')
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):
253
i_cfgs.append(util.read_conf(cc_fn))
255
util.logexc(LOG, 'Failed loading of cloud-config from %s',
260
# Input config files override
261
# env config files which
262
# override instance configs
263
# which override datasource
264
# configs which override
268
for c_fn in self._fns:
270
cfgs.append(util.read_conf(c_fn))
272
util.logexc(LOG, "Failed loading of configuration from %s",
275
cfgs.extend(self._get_env_configs())
276
cfgs.extend(self._get_instance_configs())
277
cfgs.extend(self._get_datasource_configs())
279
cfgs.append(self._base_cfg)
280
return util.mergemanydict(cfgs)
284
# None check to avoid empty case causing re-reading
285
if self._cfg is None:
286
self._cfg = self._read_cfg()
290
class ContentHandlers(object):
294
self.initialized = []
296
def __contains__(self, item):
297
return self.is_registered(item)
299
def __getitem__(self, key):
300
return self._get_handler(key)
302
def is_registered(self, content_type):
303
return content_type in self.registered
305
def register(self, mod, initialized=False, overwrite=True):
307
for t in mod.list_types():
311
if not self.is_registered(t):
314
self.registered[t] = mod
315
if initialized and mod not in self.initialized:
316
self.initialized.append(mod)
319
def _get_handler(self, content_type):
320
return self.registered[content_type]
323
return list(self.registered.items())
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')
340
"handlers": "handlers",
341
"scripts": "scripts",
342
"vendor_scripts": "scripts/vendor",
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",
351
"vendordata_raw": "vendor-data.txt",
352
"vendordata": "vendor-data.txt.i",
353
"instance_id": ".instance-id",
355
# Set when a datasource becomes active
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)
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)
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:
372
iid = self.datasource.get_instance_id()
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)
379
ipath = os.path.join(ipath, add_on)
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)
388
LOG.warn(("No per instance data available, "
389
"is there an datasource/iid set?"))
394
def _get_path(self, base, name=None):
397
return os.path.join(base, self.lookups[name])
399
def get_runpath(self, name=None):
400
return self._get_path(self.run_dir, name)
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
412
class DefaultingConfigParser(RawConfigParser):
418
def get(self, section, option):
419
value = self.DEF_BASE
421
value = RawConfigParser.get(self, section, option)
422
except NoSectionError:
424
except NoOptionError:
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)
433
def remove_option(self, section, option):
434
if self.has_option(section, option):
435
RawConfigParser.remove_option(self, section, option)
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)
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)
447
def getint(self, section, option):
448
if not self.has_option(section, option):
450
return RawConfigParser.getint(self, section, option)
452
def stringify(self, header=None):
454
with six.StringIO() as outputstream:
455
self.write(outputstream)
457
contents = outputstream.getvalue()
459
contents = "\n".join([header, contents])