30
30
# Various defaults/constants...
31
31
DEFAULT_IID = "iid-dsconfigdrive"
32
32
DEFAULT_MODE = 'pass'
33
CFG_DRIVE_FILES_V1 = [
34
34
"etc/network/interfaces",
35
35
"root/.ssh/authorized_keys",
38
38
DEFAULT_METADATA = {
39
39
"instance-id": DEFAULT_IID,
40
"dsmode": DEFAULT_MODE,
42
CFG_DRIVE_DEV_ENV = 'CLOUD_INIT_CONFIG_DRIVE_DEVICE'
41
VALID_DSMODES = ("local", "net", "pass", "disabled")
45
44
class DataSourceConfigDrive(sources.DataSource):
46
45
def __init__(self, sys_cfg, distro, paths):
47
46
sources.DataSource.__init__(self, sys_cfg, distro, paths)
50
48
self.dsmode = 'local'
51
49
self.seed_dir = os.path.join(paths.seed_dir, 'config_drive')
54
mstr = "%s [%s]" % (util.obj_name(self), self.dsmode)
55
mstr += "[seed=%s]" % (self.seed)
53
mstr = "%s [%s,ver=%s]" % (util.obj_name(self), self.dsmode,
55
mstr += "[source=%s]" % (self.source)
58
58
def get_data(self):
63
63
if os.path.isdir(self.seed_dir):
65
(md, ud) = read_config_drive_dir(self.seed_dir)
65
results = read_config_drive_dir(self.seed_dir)
66
66
found = self.seed_dir
67
67
except NonConfigDriveDir:
68
68
util.logexc(LOG, "Failed reading config drive from %s",
71
dev = find_cfg_drive_device()
71
devlist = find_candidate_devs()
74
(md, ud) = util.mount_cb(dev, read_config_drive_dir)
74
results = util.mount_cb(dev, read_config_drive_dir)
76
77
except (NonConfigDriveDir, util.MountFailedError):
79
except BrokenConfigDriveDir:
80
util.logexc(LOG, "broken config drive: %s", dev)
83
self.cfg = md['dscfg']
85
md = results['metadata']
85
86
md = util.mergedict(md, DEFAULT_METADATA)
87
# Update interfaces and ifup only on the local datasource
88
# this way the DataSourceConfigDriveNet doesn't do it also.
89
if 'network-interfaces' in md and self.dsmode == "local":
88
user_dsmode = results.get('dsmode', None)
89
if user_dsmode not in VALID_DSMODES + (None,):
90
LOG.warn("user specified invalid mode: %s" % user_dsmode)
93
dsmode = get_ds_mode(cfgdrv_ver=results['cfgdrive_ver'],
94
ds_cfg=self.ds_cfg.get('dsmode'),
97
if dsmode == "disabled":
98
# most likely user specified
101
# TODO(smoser): fix this, its dirty.
102
# we want to do some things (writing files and network config)
103
# only on first boot, and even then, we want to do so in the
104
# local datasource (so they happen earlier) even if the configured
105
# dsmode is 'net' or 'pass'. To do this, we check the previous
107
prev_iid = get_previous_iid(self.paths)
108
cur_iid = md['instance-id']
110
if ('network_config' in results and self.dsmode == "local" and
111
prev_iid != cur_iid):
90
112
LOG.debug("Updating network interfaces from config drive (%s)",
92
self.distro.apply_network(md['network-interfaces'])
114
self.distro.apply_network(results['network_config'])
116
# file writing occurs in local mode (to be as early as possible)
117
if self.dsmode == "local" and prev_iid != cur_iid and results['files']:
118
LOG.debug("writing injected files")
120
write_files(results['files'])
122
util.logexc(LOG, "Failed writing files")
124
# dsmode != self.dsmode here if:
125
# * dsmode = "pass", pass means it should only copy files and then
126
# pass to another datasource
127
# * dsmode = "net" and self.dsmode = "local"
128
# so that user boothooks would be applied with network, the
129
# local datasource just gets out of the way, and lets the net claim
130
if dsmode != self.dsmode:
131
LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode)
95
135
self.metadata = md
96
self.userdata_raw = ud
98
if md['dsmode'] == self.dsmode:
101
LOG.debug("%s: not claiming datasource, dsmode=%s", self, md['dsmode'])
136
self.userdata_raw = results.get('userdata')
137
self.version = results['cfgdrive_ver']
104
141
def get_public_ssh_keys(self):
105
142
if not 'public-keys' in self.metadata:
107
144
return self.metadata['public-keys']
109
# The data sources' config_obj is a cloud-config formated
110
# object that came to it from ways other than cloud-config
111
# because cloud-config content would be handled elsewhere
112
def get_config_obj(self):
116
147
class DataSourceConfigDriveNet(DataSourceConfigDrive):
117
148
def __init__(self, sys_cfg, distro, paths):
126
def find_cfg_drive_device():
127
"""Get the config drive device. Return a string like '/dev/vdb'
128
or None (if there is no non-root device attached). This does not
129
check the contents, only reports that if there *were* a config_drive
130
attached, it would be this device.
131
Note: per config_drive documentation, this is
132
"associated as the last available disk on the instance"
157
class BrokenConfigDriveDir(Exception):
161
def find_candidate_devs():
162
"""Return a list of devices that may contain the config drive.
164
The returned list is sorted by search order where the first item has
165
should be searched first (highest priority)
168
Per documentation, this is "associated as the last available disk on the
169
instance", and should be VFAT.
170
Currently, we do not restrict search list to "last available disk"
174
* either vfat or iso9660 formated
175
* labeled with 'config-2'
135
# This seems to be for debugging??
136
if CFG_DRIVE_DEV_ENV in os.environ:
137
return os.environ[CFG_DRIVE_DEV_ENV]
139
# We are looking for a raw block device (sda, not sda1) with a vfat
140
# filesystem on it....
141
letters = "abcdefghijklmnopqrstuvwxyz"
142
devs = util.find_devs_with("TYPE=vfat")
144
# Filter out anything not ending in a letter (ignore partitions)
145
devs = [f for f in devs if f[-1] in letters]
147
# Sort them in reverse so "last" device is first
148
devs.sort(reverse=True)
178
by_fstype = (util.find_devs_with("TYPE=vfat") +
179
util.find_devs_with("TYPE=iso9660"))
180
by_label = util.find_devs_with("LABEL=config-2")
182
# give preference to "last available disk" (vdb over vda)
183
# note, this is not a perfect rendition of that.
184
by_fstype.sort(reverse=True)
185
by_label.sort(reverse=True)
187
# combine list of items by putting by-label items first
188
# followed by fstype items, but with dupes removed
189
combined = (by_label + [d for d in by_fstype if d not in by_label])
191
# We are looking for block device (sda, not sda1), ignore partitions
192
combined = [d for d in combined if d[-1] not in "0123456789"]
156
197
def read_config_drive_dir(source_dir):
158
read_config_drive_dir(source_dir):
159
read source_dir, and return a tuple with metadata dict and user-data
160
string populated. If not a valid dir, raise a NonConfigDriveDir
163
# TODO(harlowja): fix this for other operating systems...
164
# Ie: this is where https://fedorahosted.org/netcf/ or similar should
165
# be hooked in... (or could be)
198
last_e = NonConfigDriveDir("Not found")
199
for finder in (read_config_drive_dir_v2, read_config_drive_dir_v1):
201
data = finder(source_dir)
203
except NonConfigDriveDir as exc:
208
def read_config_drive_dir_v2(source_dir, version="2012-08-10"):
210
if (not os.path.isdir(os.path.join(source_dir, "openstack", version)) and
211
os.path.isdir(os.path.join(source_dir, "openstack", "latest"))):
212
LOG.warn("version '%s' not available, attempting to use 'latest'" %
218
"openstack/%s/meta_data.json" % version, True, json.loads),
219
('userdata', "openstack/%s/user_data" % version, False, None),
220
('ec2-metadata', "ec2/latest/metadata.json", False, json.loads),
223
results = {'userdata': None}
224
for (name, path, required, process) in datafiles:
225
fpath = os.path.join(source_dir, path)
228
if os.path.isfile(fpath):
230
with open(fpath) as fp:
232
except Exception as exc:
233
raise BrokenConfigDriveDir("failed to read: %s" % fpath)
236
raise NonConfigDriveDir("missing mandatory %s" % fpath)
238
if found and process:
241
except Exception as exc:
242
raise BrokenConfigDriveDir("failed to process: %s" % fpath)
247
# instance-id is 'uuid' for openstack. just copy it to instance-id.
248
if 'instance-id' not in results['metadata']:
250
results['metadata']['instance-id'] = results['metadata']['uuid']
252
raise BrokenConfigDriveDir("No uuid entry in metadata")
254
def read_content_path(item):
255
# do not use os.path.join here, as content_path starts with /
256
cpath = os.path.sep.join((source_dir, "openstack",
257
"./%s" % item['content_path']))
258
with open(cpath) as fp:
263
for item in results['metadata'].get('files', {}):
264
files[item['path']] = read_content_path(item)
266
# the 'network_config' item in metadata is a content pointer
267
# to the network config that should be applied.
268
# in folsom, it is just a '/etc/network/interfaces' file.
269
item = results['metadata'].get("network_config", None)
271
results['network_config'] = read_content_path(item)
272
except Exception as exc:
273
raise BrokenConfigDriveDir("failed to read file %s: %s" % (item, exc))
275
# to openstack, user can specify meta ('nova boot --meta=key=value') and
276
# those will appear under metadata['meta'].
277
# if they specify 'dsmode' they're indicating the mode that they intend
278
# for this datasource to operate in.
280
results['dsmode'] = results['metadata']['meta']['dsmode']
284
results['files'] = files
285
results['cfgdrive_ver'] = 2
289
def read_config_drive_dir_v1(source_dir):
291
read source_dir, and return a tuple with metadata dict, user-data,
292
files and version (1). If not a valid dir, raise a NonConfigDriveDir
167
for af in CFG_DRIVE_FILES:
296
for af in CFG_DRIVE_FILES_V1:
168
297
fn = os.path.join(source_dir, af)
169
298
if os.path.isfile(fn):
197
325
(source_dir, "invalid json in meta.js", e))
198
326
md['meta_js'] = content
200
# Key data override??
328
# keydata in meta_js is preferred over "injected"
201
329
keydata = meta_js.get('public-keys', keydata)
203
331
lines = keydata.splitlines()
204
332
md['public-keys'] = [l for l in lines
205
333
if len(l) and not l.startswith("#")]
207
for copy in ('dsmode', 'instance-id', 'dscfg'):
209
md[copy] = meta_js[copy]
211
if 'user-data' in meta_js:
212
ud = meta_js['user-data']
335
# config-drive-v1 has no way for openstack to provide the instance-id
336
# so we copy that into metadata from the user input
337
if 'instance-id' in meta_js:
338
md['instance-id'] = meta_js['instance-id']
340
results = {'cfgdrive_ver': 1, 'metadata': md}
342
# allow the user to specify 'dsmode' in a meta tag
343
if 'dsmode' in meta_js:
344
results['dsmode'] = meta_js['dsmode']
346
# config-drive-v1 has no way of specifying user-data, so the user has
347
# to cheat and stuff it in a meta tag also.
348
results['userdata'] = meta_js.get('user-data')
350
# this implementation does not support files
351
# (other than network/interfaces and authorized_keys)
352
results['files'] = []
357
def get_ds_mode(cfgdrv_ver, ds_cfg=None, user=None):
358
"""Determine what mode should be used.
359
valid values are 'pass', 'disabled', 'local', 'net'
361
# user passed data trumps everything
365
if ds_cfg is not None:
368
# at config-drive version 1, the default behavior was pass. That
369
# meant to not use use it as primary data source, but expect a ec2 metadata
370
# source. for version 2, we default to 'net', which means
371
# the DataSourceConfigDriveNet, would be used.
373
# this could change in the future. If there was definitive metadata
374
# that indicated presense of an openstack metadata service, then
375
# we could change to 'pass' by default also. The motivation for that
376
# would be 'cloud-init query' as the web service could be more dynamic
382
def get_previous_iid(paths):
383
# interestingly, for this purpose the "previous" instance-id is the current
384
# instance-id. cloud-init hasn't moved them over yet as this datasource
385
# hasn't declared itself found.
386
fname = os.path.join(paths.get_cpath('data'), 'instance-id')
388
with open(fname) as fp:
394
def write_files(files):
395
for (name, content) in files.iteritems():
396
if name[0] != os.sep:
398
util.write_file(name, content, mode=0660)
217
401
# Used to match classes to dependencies