3
# Common code for the EC2 initialisation scripts in Ubuntu
4
# Copyright (C) 2008-2009 Canonical Ltd
6
# Author: Soren Hansen <soren@canonical.com>
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License version 3, as
10
# published by the Free Software Foundation.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
varlibdir = '/var/lib/cloud'
22
cur_instance_link = varlibdir + "/instance"
23
boot_finished = cur_instance_link + "/boot-finished"
24
system_config = '/etc/cloud/cloud.cfg'
25
seeddir = varlibdir + "/seed"
26
cfg_env_name = "CLOUD_CFG"
30
datasource_list: [ "NoCloud", "OVF", "Ec2" ]
31
def_log_file: /var/log/cloud-init.log
32
syslog_fix_perms: syslog:adm
34
logger_name = "cloudinit"
37
"handlers" : "/handlers",
38
"scripts" : "/scripts",
40
"boothooks" : "/boothooks",
41
"userdata_raw" : "/user-data.txt",
42
"userdata" : "/user-data-raw.txt.i",
43
"obj_pkl" : "/obj.pkl",
44
"cloud_config" : "/cloud-config.txt",
52
from configobj import ConfigObj
67
class NullHandler(logging.Handler):
68
def emit(self,record): pass
70
log = logging.getLogger(logger_name)
71
log.addHandler(NullHandler())
73
def logging_set_from_cfg_file(cfg_file=system_config):
74
logging_set_from_cfg(util.get_base_cfg(cfg_file,cfg_builtin,parsed_cfgs))
76
def logging_set_from_cfg(cfg):
78
logcfg=util.get_cfg_option_str(cfg, "log_cfg", False)
80
# if there is a 'logcfg' entry in the config, respect
81
# it, it is the old keyname
83
elif "log_cfgs" in cfg:
84
for cfg in cfg['log_cfgs']:
85
if isinstance(cfg,list):
86
log_cfgs.append('\n'.join(cfg))
91
sys.stderr.write("Warning, no logging configured\n")
94
for logcfg in log_cfgs:
96
logging.config.fileConfig(StringIO.StringIO(logcfg))
101
raise Exception("no valid logging found\n")
105
import UserDataHandler
110
old_conffile = '/etc/ec2-init/ec2-config.cfg'
111
ds_deps = [ DataSource.DEP_FILESYSTEM, DataSource.DEP_NETWORK ]
114
def __init__(self, ds_deps = None, sysconfig=system_config):
115
self.part_handlers = {
116
'text/x-shellscript' : self.handle_user_script,
117
'text/cloud-config' : self.handle_cloud_config,
118
'text/upstart-job' : self.handle_upstart_job,
119
'text/part-handler' : self.handle_handler,
120
'text/cloud-boothook' : self.handle_cloud_boothook
123
self.ds_deps = ds_deps
124
self.sysconfig=sysconfig
125
self.cfg=self.read_cfg()
132
conf = util.get_base_cfg(self.sysconfig,cfg_builtin, parsed_cfgs)
133
except Exception as e:
134
conf = get_builtin_cfg()
136
# support reading the old ConfigObj format file and merging
137
# it into the yaml dictionary
139
from configobj import ConfigObj
140
oldcfg = ConfigObj(self.old_conffile)
141
if oldcfg is None: oldcfg = { }
142
conf = util.mergedict(conf,oldcfg)
148
def restore_from_cache(self):
150
# we try to restore from a current link and static path
151
# by using the instance link, if purge_cache was called
152
# the file wont exist
153
cache = get_ipath_cur('obj_pkl')
155
data = cPickle.load(f)
156
self.datasource = data
161
def write_to_cache(self):
162
cache = self.get_ipath("obj_pkl")
164
os.makedirs(os.path.dirname(cache))
166
if e.errno != errno.EEXIST:
171
data = cPickle.dump(self.datasource,f)
177
def get_data_source(self):
178
if self.datasource is not None: return True
180
if self.restore_from_cache():
181
log.debug("restored from cache type %s" % self.datasource)
184
cfglist=self.cfg['datasource_list']
185
dslist = list_sources(cfglist, self.ds_deps)
186
dsnames = map(lambda f: f.__name__, dslist)
187
log.debug("searching for data source in %s" % dsnames)
194
self.datasource_name = ds
195
log.debug("found data source %s" % ds)
197
except Exception as e:
198
log.warn("get_data of %s raised %s" % (ds,e))
201
msg = "Did not find data source. searched classes: %s" % dsnames
203
raise DataSourceNotFoundException(msg)
205
def set_cur_instance(self):
207
os.unlink(cur_instance_link)
209
if e.errno != errno.ENOENT: raise
211
os.symlink("./instances/%s" % self.get_instance_id(), cur_instance_link)
212
idir = self.get_ipath()
214
for d in [ "handlers", "scripts", "sem" ]:
215
dlist.append("%s/%s" % (idir, d))
217
util.ensure_dirs(dlist)
219
def get_userdata(self):
220
return(self.datasource.get_userdata())
222
def get_userdata_raw(self):
223
return(self.datasource.get_userdata_raw())
225
def get_instance_id(self):
226
return(self.datasource.get_instance_id())
228
def update_cache(self):
229
self.write_to_cache()
230
self.store_userdata()
232
def store_userdata(self):
233
util.write_file(self.get_ipath('userdata_raw'),
234
self.datasource.get_userdata_raw(), 0600)
235
util.write_file(self.get_ipath('userdata'),
236
self.datasource.get_userdata(), 0600)
238
def initctl_emit(self):
239
cc_path = get_ipath_cur('cloud_config')
240
subprocess.Popen(['initctl', 'emit', 'cloud-config',
241
'%s=%s' % (cfg_env_name,cc_path)]).communicate()
243
def sem_getpath(self,name,freq):
244
if freq == 'once-per-instance':
245
return("%s/%s" % (self.get_ipath("sem"),name))
246
return("%s/%s.%s" % (get_cpath("sem"), name, freq))
248
def sem_has_run(self,name,freq):
249
if freq == "always": return False
250
semfile = self.sem_getpath(name,freq)
251
if os.path.exists(semfile):
255
def sem_acquire(self,name,freq):
256
from time import time
257
semfile = self.sem_getpath(name,freq)
260
os.makedirs(os.path.dirname(semfile))
262
if e.errno != errno.EEXIST:
265
if os.path.exists(semfile) and freq != "always":
270
f = open(semfile,"w")
271
f.write("%s\n" % str(time()))
277
def sem_clear(self,name,freq):
278
semfile = self.sem_getpath(name,freq)
282
if e.errno != errno.ENOENT:
287
# acquire lock on 'name' for given 'freq'
288
# if that does not exist, then call 'func' with given 'args'
289
# if 'clear_on_fail' is True and func throws an exception
290
# then remove the lock (so it would run again)
291
def sem_and_run(self,semname,freq,func,args=[],clear_on_fail=False):
292
if self.sem_has_run(semname,freq):
293
log.debug("%s already ran %s", semname, freq)
296
if not self.sem_acquire(semname,freq):
297
raise Exception("Failed to acquire lock on %s" % semname)
302
self.sem_clear(semname,freq)
305
# get_ipath : get the instance path for a name in pathmap
306
# (/var/lib/cloud/instances/<instance>/name)<name>)
307
def get_ipath(self, name=None):
308
return("%s/instances/%s%s"
309
% (varlibdir,self.get_instance_id(), pathmap[name]))
311
def consume_userdata(self):
315
cdir = get_cpath("handlers")
316
idir = self.get_ipath("handlers")
318
# add the path to the plugins dir to the top of our list for import
319
# instance dir should be read before cloud-dir
320
sys.path.insert(0,cdir)
321
sys.path.insert(0,idir)
323
# add handlers in cdir
324
for fname in glob.glob("%s/*.py" % cdir):
325
if not os.path.isfile(fname): continue
326
modname = os.path.basename(fname)[0:-3]
328
mod = __import__(modname)
329
lister = getattr(mod, "list_types")
330
handler = getattr(mod, "handle_part")
333
self.part_handlers[mtype]=handler
334
log.debug("added handler for [%s] from %s" % (mtypes,fname))
336
log.warn("failed to initialize handler in %s" % fname)
339
# give callbacks opportunity to initialize
340
for ctype, func in self.part_handlers.items():
341
func(data, "__begin__",None,None)
342
UserDataHandler.walk_userdata(self.get_userdata(),
343
self.part_handlers, data)
345
# give callbacks opportunity to finalize
346
for ctype, func in self.part_handlers.items():
347
func(data,"__end__",None,None)
349
def handle_handler(self,data,ctype,filename,payload):
350
if ctype == "__end__": return
351
if ctype == "__begin__" :
352
self.handlercount = 0
355
self.handlercount=self.handlercount+1
357
# write content to instance's handlerdir
358
handlerdir = self.get_ipath("handler")
359
modname = 'part-handler-%03d' % self.handlercount
360
modfname = modname + ".py"
361
util.write_file("%s/%s" % (handlerdir,modfname), payload, 0600)
364
mod = __import__(modname)
365
lister = getattr(mod, "list_types")
366
handler = getattr(mod, "handle_part")
369
traceback.print_exc(file=sys.stderr)
372
# - call it with '__begin__'
373
handler(data, "__begin__", None, None)
375
# - add it self.part_handlers
376
for mtype in lister():
377
self.part_handlers[mtype]=handler
379
def handle_user_script(self,data,ctype,filename,payload):
380
if ctype == "__end__": return
381
if ctype == "__begin__":
382
# maybe delete existing things here
385
filename=filename.replace(os.sep,'_')
386
scriptsdir = get_ipath_cur('scripts')
387
util.write_file("%s/%s/%s" %
388
(scriptsdir,self.get_instance_id(),filename), payload, 0700)
390
def handle_upstart_job(self,data,ctype,filename,payload):
391
if ctype == "__end__" or ctype == "__begin__": return
392
if not filename.endswith(".conf"):
393
filename=filename+".conf"
395
util.write_file("%s/%s" % ("/etc/init",filename), payload, 0644)
397
def handle_cloud_config(self,data,ctype,filename,payload):
398
if ctype == "__begin__":
399
self.cloud_config_str=""
401
if ctype == "__end__":
402
cloud_config = self.get_ipath("cloud_config")
403
util.write_file(cloud_config, self.cloud_config_str, 0600)
405
## this could merge the cloud config with the system config
406
## for now, not doing this as it seems somewhat circular
407
## as CloudConfig does that also, merging it with this cfg
409
# ccfg = yaml.load(self.cloud_config_str)
410
# if ccfg is None: ccfg = { }
411
# self.cfg = util.mergedict(ccfg, self.cfg)
415
self.cloud_config_str+="\n#%s\n%s" % (filename,payload)
417
def handle_cloud_boothook(self,data,ctype,filename,payload):
418
if ctype == "__end__": return
419
if ctype == "__begin__": return
421
filename=filename.replace(os.sep,'_')
422
prefix="#cloud-boothook"
425
if payload.startswith(prefix):
427
if payload[start] == '\r':
431
if payload.find('\r\n',0,100) >= 0:
435
payload=payload[start:].replace('\r\n','\n')
437
payload=payload[start:]
439
boothooks_dir = self.get_ipath("boothooks")
440
filepath = "%s/%s" % (boothooks_dir,filename)
441
util.write_file(filepath, payload, 0700)
443
env=os.environ.copy()
444
env['INSTANCE_ID']= self.datasource.get_instance_id()
445
ret = subprocess.check_call([filepath], env=env)
446
except subprocess.CalledProcessError as e:
447
log.error("boothooks script %s returned %i" %
448
(filepath,e.returncode))
449
except Exception as e:
450
log.error("boothooks unknown exception %s when running %s" %
453
def get_public_ssh_keys(self):
454
return(self.datasource.get_public_ssh_keys())
456
def get_locale(self):
457
return(self.datasource.get_locale())
459
def get_mirror(self):
460
return(self.datasource.get_local_mirror())
462
def get_hostname(self):
463
return(self.datasource.get_hostname())
465
def device_name_to_device(self,name):
466
return(self.datasource.device_name_to_device(name))
468
# I really don't know if this should be here or not, but
469
# I needed it in cc_update_hostname, where that code had a valid 'cloud'
470
# reference, but did not have a cloudinit handle
471
# (ie, no cloudinit.get_cpath())
472
def get_cpath(self,name=None):
473
return(get_cpath(name))
477
subds = [ 'scripts/per-instance', 'scripts/per-once', 'scripts/per-boot',
478
'seed', 'instances', 'handlers', 'sem', 'data' ]
481
dlist.append("%s/%s" % (varlibdir, subd))
482
util.ensure_dirs(dlist)
484
cfg = util.get_base_cfg(system_config,cfg_builtin,parsed_cfgs)
485
log_file = util.get_cfg_option_str(cfg, 'def_log_file', None)
486
perms = util.get_cfg_option_str(cfg, 'syslog_fix_perms', None)
488
fp = open(log_file,"ab")
490
if log_file and perms:
491
(u,g) = perms.split(':',1)
492
if u == "-1" or u == "None": u = None
493
if g == "-1" or g == "None": g = None
494
util.chownbyname(log_file, u, g)
497
rmlist = ( boot_finished , cur_instance_link )
502
if e.errno == errno.ENOENT: continue
508
# get_ipath_cur: get the current instance path for an item
509
def get_ipath_cur(name=None):
510
return("%s/instance/%s" % (varlibdir, pathmap[name]))
512
# get_cpath : get the "clouddir" (/var/lib/cloud/<name>)
513
# for a name in dirmap
514
def get_cpath(name=None):
515
return("%s%s" % (varlibdir, pathmap[name]))
517
def get_base_cfg(cfg_path=None):
518
if cfg_path is None: cfg_path = system_config
519
return(util.get_base_cfg(cfg_path,cfg_builtin,parsed_cfgs))
521
def get_builtin_cfg():
522
return(yaml.load(cfg_builtin))
524
class DataSourceNotFoundException(Exception):
527
def list_sources(cfg_list, depends):
528
return(DataSource.list_sources(cfg_list,depends, ["cloudinit", "" ]))