~ubuntu-branches/ubuntu/saucy/cloud-init/saucy

« back to all changes in this revision

Viewing changes to cloudinit/sources/DataSourceOpenNebula.py

  • Committer: Scott Moser
  • Date: 2013-09-11 21:04:19 UTC
  • mfrom: (1.4.5)
  • Revision ID: smoser@ubuntu.com-20130911210419-3vt5ze6ph3hu8dz1
* New upstream snapshot.
  * Add OpenNebula datasource.
  * Support reading 'random_seed' from metadata and writing to /dev/urandom
  * fix for bug in log_time.

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 Yahoo! Inc.
 
5
#    Copyright (C) 2012-2013 CERIT Scientific Cloud
 
6
#    Copyright (C) 2012-2013 OpenNebula.org
 
7
#
 
8
#    Author: Scott Moser <scott.moser@canonical.com>
 
9
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
 
10
#    Author: Vlastimil Holer <xholer@mail.muni.cz>
 
11
#    Author: Javier Fontan <jfontan@opennebula.org>
 
12
#
 
13
#    This program is free software: you can redistribute it and/or modify
 
14
#    it under the terms of the GNU General Public License version 3, as
 
15
#    published by the Free Software Foundation.
 
16
#
 
17
#    This program is distributed in the hope that it will be useful,
 
18
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
19
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
20
#    GNU General Public License for more details.
 
21
#
 
22
#    You should have received a copy of the GNU General Public License
 
23
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
24
 
 
25
import os
 
26
import pwd
 
27
import re
 
28
import string  # pylint: disable=W0402
 
29
 
 
30
from cloudinit import log as logging
 
31
from cloudinit import sources
 
32
from cloudinit import util
 
33
 
 
34
LOG = logging.getLogger(__name__)
 
35
 
 
36
DEFAULT_IID = "iid-dsopennebula"
 
37
DEFAULT_MODE = 'net'
 
38
DEFAULT_PARSEUSER = 'nobody'
 
39
CONTEXT_DISK_FILES = ["context.sh"]
 
40
VALID_DSMODES = ("local", "net", "disabled")
 
41
 
 
42
 
 
43
class DataSourceOpenNebula(sources.DataSource):
 
44
    def __init__(self, sys_cfg, distro, paths):
 
45
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
 
46
        self.dsmode = 'local'
 
47
        self.seed = None
 
48
        self.seed_dir = os.path.join(paths.seed_dir, 'opennebula')
 
49
 
 
50
    def __str__(self):
 
51
        root = sources.DataSource.__str__(self)
 
52
        return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
 
53
 
 
54
    def get_data(self):
 
55
        defaults = {"instance-id": DEFAULT_IID}
 
56
        results = None
 
57
        seed = None
 
58
 
 
59
        # decide parseuser for context.sh shell reader
 
60
        parseuser = DEFAULT_PARSEUSER
 
61
        if 'parseuser' in self.ds_cfg:
 
62
            parseuser = self.ds_cfg.get('parseuser')
 
63
 
 
64
        candidates = [self.seed_dir]
 
65
        candidates.extend(find_candidate_devs())
 
66
        for cdev in candidates:
 
67
            try:
 
68
                if os.path.isdir(self.seed_dir):
 
69
                    results = read_context_disk_dir(cdev, asuser=parseuser)
 
70
                elif cdev.startswith("/dev"):
 
71
                    results = util.mount_cb(cdev, read_context_disk_dir,
 
72
                                            data=parseuser)
 
73
            except NonContextDiskDir:
 
74
                continue
 
75
            except BrokenContextDiskDir as exc:
 
76
                raise exc
 
77
            except util.MountFailedError:
 
78
                LOG.warn("%s was not mountable" % cdev)
 
79
 
 
80
            if results:
 
81
                seed = cdev
 
82
                LOG.debug("found datasource in %s", cdev)
 
83
                break
 
84
 
 
85
        if not seed:
 
86
            return False
 
87
 
 
88
        # merge fetched metadata with datasource defaults
 
89
        md = results['metadata']
 
90
        md = util.mergemanydict([md, defaults])
 
91
 
 
92
        # check for valid user specified dsmode
 
93
        user_dsmode = results['metadata'].get('DSMODE', None)
 
94
        if user_dsmode not in VALID_DSMODES + (None,):
 
95
            LOG.warn("user specified invalid mode: %s", user_dsmode)
 
96
            user_dsmode = None
 
97
 
 
98
        # decide dsmode
 
99
        if user_dsmode:
 
100
            dsmode = user_dsmode
 
101
        elif self.ds_cfg.get('dsmode'):
 
102
            dsmode = self.ds_cfg.get('dsmode')
 
103
        else:
 
104
            dsmode = DEFAULT_MODE
 
105
 
 
106
        if dsmode == "disabled":
 
107
            # most likely user specified
 
108
            return False
 
109
 
 
110
        # apply static network configuration only in 'local' dsmode
 
111
        if ('network-interfaces' in results and self.dsmode == "local"):
 
112
            LOG.debug("Updating network interfaces from %s", self)
 
113
            self.distro.apply_network(results['network-interfaces'])
 
114
 
 
115
        if dsmode != self.dsmode:
 
116
            LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode)
 
117
            return False
 
118
 
 
119
        self.seed = seed
 
120
        self.metadata = md
 
121
        self.userdata_raw = results.get('userdata')
 
122
        return True
 
123
 
 
124
    def get_hostname(self, fqdn=False, resolve_ip=None):
 
125
        if resolve_ip is None:
 
126
            if self.dsmode == 'net':
 
127
                resolve_ip = True
 
128
            else:
 
129
                resolve_ip = False
 
130
        return sources.DataSource.get_hostname(self, fqdn, resolve_ip)
 
131
 
 
132
 
 
133
class DataSourceOpenNebulaNet(DataSourceOpenNebula):
 
134
    def __init__(self, sys_cfg, distro, paths):
 
135
        DataSourceOpenNebula.__init__(self, sys_cfg, distro, paths)
 
136
        self.dsmode = 'net'
 
137
 
 
138
 
 
139
class NonContextDiskDir(Exception):
 
140
    pass
 
141
 
 
142
 
 
143
class BrokenContextDiskDir(Exception):
 
144
    pass
 
145
 
 
146
 
 
147
class OpenNebulaNetwork(object):
 
148
    REG_DEV_MAC = re.compile(
 
149
                    r'^\d+: (eth\d+):.*?link\/ether (..:..:..:..:..:..) ?',
 
150
                    re.MULTILINE | re.DOTALL)
 
151
 
 
152
    def __init__(self, ip, context):
 
153
        self.ip = ip
 
154
        self.context = context
 
155
        self.ifaces = self.get_ifaces()
 
156
 
 
157
    def get_ifaces(self):
 
158
        return self.REG_DEV_MAC.findall(self.ip)
 
159
 
 
160
    def mac2ip(self, mac):
 
161
        components = mac.split(':')[2:]
 
162
        return [str(int(c, 16)) for c in components]
 
163
 
 
164
    def get_ip(self, dev, components):
 
165
        var_name = dev.upper() + '_IP'
 
166
        if var_name in self.context:
 
167
            return self.context[var_name]
 
168
        else:
 
169
            return '.'.join(components)
 
170
 
 
171
    def get_mask(self, dev):
 
172
        var_name = dev.upper() + '_MASK'
 
173
        if var_name in self.context:
 
174
            return self.context[var_name]
 
175
        else:
 
176
            return '255.255.255.0'
 
177
 
 
178
    def get_network(self, dev, components):
 
179
        var_name = dev.upper() + '_NETWORK'
 
180
        if var_name in self.context:
 
181
            return self.context[var_name]
 
182
        else:
 
183
            return '.'.join(components[:-1]) + '.0'
 
184
 
 
185
    def get_gateway(self, dev):
 
186
        var_name = dev.upper() + '_GATEWAY'
 
187
        if var_name in self.context:
 
188
            return self.context[var_name]
 
189
        else:
 
190
            return None
 
191
 
 
192
    def get_dns(self, dev):
 
193
        var_name = dev.upper() + '_DNS'
 
194
        if var_name in self.context:
 
195
            return self.context[var_name]
 
196
        else:
 
197
            return None
 
198
 
 
199
    def get_domain(self, dev):
 
200
        var_name = dev.upper() + '_DOMAIN'
 
201
        if var_name in self.context:
 
202
            return self.context[var_name]
 
203
        else:
 
204
            return None
 
205
 
 
206
    def gen_conf(self):
 
207
        global_dns = []
 
208
        if 'DNS' in self.context:
 
209
            global_dns.append(self.context['DNS'])
 
210
 
 
211
        conf = []
 
212
        conf.append('auto lo')
 
213
        conf.append('iface lo inet loopback')
 
214
        conf.append('')
 
215
 
 
216
        for i in self.ifaces:
 
217
            dev = i[0]
 
218
            mac = i[1]
 
219
            ip_components = self.mac2ip(mac)
 
220
 
 
221
            conf.append('auto ' + dev)
 
222
            conf.append('iface ' + dev + ' inet static')
 
223
            conf.append('  address ' + self.get_ip(dev, ip_components))
 
224
            conf.append('  network ' + self.get_network(dev, ip_components))
 
225
            conf.append('  netmask ' + self.get_mask(dev))
 
226
 
 
227
            gateway = self.get_gateway(dev)
 
228
            if gateway:
 
229
                conf.append('  gateway ' + gateway)
 
230
 
 
231
            domain = self.get_domain(dev)
 
232
            if domain:
 
233
                conf.append('  dns-search ' + domain)
 
234
 
 
235
            # add global DNS servers to all interfaces
 
236
            dns = self.get_dns(dev)
 
237
            if global_dns or dns:
 
238
                all_dns = global_dns
 
239
                if dns:
 
240
                    all_dns.append(dns)
 
241
                conf.append('  dns-nameservers ' + ' '.join(all_dns))
 
242
 
 
243
            conf.append('')
 
244
 
 
245
        return "\n".join(conf)
 
246
 
 
247
 
 
248
def find_candidate_devs():
 
249
    """
 
250
    Return a list of devices that may contain the context disk.
 
251
    """
 
252
    combined = []
 
253
    for f in ('LABEL=CONTEXT', 'LABEL=CDROM', 'TYPE=iso9660'):
 
254
        devs = util.find_devs_with(f)
 
255
        devs.sort()
 
256
        for d in devs:
 
257
            if d not in combined:
 
258
                combined.append(d)
 
259
 
 
260
    return combined
 
261
 
 
262
 
 
263
def switch_user_cmd(user):
 
264
    return ['sudo', '-u', user]
 
265
 
 
266
 
 
267
def parse_shell_config(content, keylist=None, bash=None, asuser=None,
 
268
                       switch_user_cb=None):
 
269
 
 
270
    if isinstance(bash, str):
 
271
        bash = [bash]
 
272
    elif bash is None:
 
273
        bash = ['bash', '-e']
 
274
 
 
275
    if switch_user_cb is None:
 
276
        switch_user_cb = switch_user_cmd
 
277
 
 
278
    # allvars expands to all existing variables by using '${!x*}' notation
 
279
    # where x is lower or upper case letters or '_'
 
280
    allvars = ["${!%s*}" % x for x in string.letters + "_"]
 
281
 
 
282
    keylist_in = keylist
 
283
    if keylist is None:
 
284
        keylist = allvars
 
285
        keylist_in = []
 
286
 
 
287
    setup = '\n'.join(('__v="";', '',))
 
288
 
 
289
    def varprinter(vlist):
 
290
        # output '\0'.join(['_start_', key=value NULL for vars in vlist]
 
291
        return '\n'.join((
 
292
            'printf "%s\\0" _start_',
 
293
            'for __v in %s; do' % ' '.join(vlist),
 
294
            '   printf "%s=%s\\0" "$__v" "${!__v}";',
 
295
            'done',
 
296
            ''
 
297
        ))
 
298
 
 
299
    # the rendered 'bcmd' is bash syntax that does
 
300
    # setup: declare variables we use (so they show up in 'all')
 
301
    # varprinter(allvars): print all variables known at beginning
 
302
    # content: execute the provided content
 
303
    # varprinter(keylist): print all variables known after content
 
304
    #
 
305
    # output is then a null terminated array of:
 
306
    #   literal '_start_'
 
307
    #   key=value (for each preset variable)
 
308
    #   literal '_start_'
 
309
    #   key=value (for each post set variable)
 
310
    bcmd = ('unset IFS\n' +
 
311
            setup +
 
312
            varprinter(allvars) +
 
313
            '{\n%s\n\n:\n} > /dev/null\n' % content +
 
314
            'unset IFS\n' +
 
315
            varprinter(keylist) + "\n")
 
316
 
 
317
    cmd = []
 
318
    if asuser is not None:
 
319
        cmd = switch_user_cb(asuser)
 
320
 
 
321
    cmd.extend(bash)
 
322
 
 
323
    (output, _error) = util.subp(cmd, data=bcmd)
 
324
 
 
325
    # exclude vars in bash that change on their own or that we used
 
326
    excluded = ("RANDOM", "LINENO", "_", "__v")
 
327
    preset = {}
 
328
    ret = {}
 
329
    target = None
 
330
    output = output[0:-1]  # remove trailing null
 
331
 
 
332
    # go through output.  First _start_ is for 'preset', second for 'target'.
 
333
    # Add to target only things were changed and not in volitile
 
334
    for line in output.split("\x00"):
 
335
        try:
 
336
            (key, val) = line.split("=", 1)
 
337
            if target is preset:
 
338
                target[key] = val
 
339
            elif (key not in excluded and
 
340
                  (key in keylist_in or preset.get(key) != val)):
 
341
                ret[key] = val
 
342
        except ValueError:
 
343
            if line != "_start_":
 
344
                raise
 
345
            if target is None:
 
346
                target = preset
 
347
            elif target is preset:
 
348
                target = ret
 
349
 
 
350
    return ret
 
351
 
 
352
 
 
353
def read_context_disk_dir(source_dir, asuser=None):
 
354
    """
 
355
    read_context_disk_dir(source_dir):
 
356
    read source_dir and return a tuple with metadata dict and user-data
 
357
    string populated.  If not a valid dir, raise a NonContextDiskDir
 
358
    """
 
359
    found = {}
 
360
    for af in CONTEXT_DISK_FILES:
 
361
        fn = os.path.join(source_dir, af)
 
362
        if os.path.isfile(fn):
 
363
            found[af] = fn
 
364
 
 
365
    if not found:
 
366
        raise NonContextDiskDir("%s: %s" % (source_dir, "no files found"))
 
367
 
 
368
    context = {}
 
369
    results = {'userdata': None, 'metadata': {}}
 
370
 
 
371
    if "context.sh" in found:
 
372
        if asuser is not None:
 
373
            try:
 
374
                pwd.getpwnam(asuser)
 
375
            except KeyError as e:
 
376
                raise BrokenContextDiskDir("configured user '%s' "
 
377
                                           "does not exist", asuser)
 
378
        try:
 
379
            with open(os.path.join(source_dir, 'context.sh'), 'r') as f:
 
380
                content = f.read().strip()
 
381
 
 
382
            context = parse_shell_config(content, asuser=asuser)
 
383
        except util.ProcessExecutionError as e:
 
384
            raise BrokenContextDiskDir("Error processing context.sh: %s" % (e))
 
385
        except IOError as e:
 
386
            raise NonContextDiskDir("Error reading context.sh: %s" % (e))
 
387
    else:
 
388
        raise NonContextDiskDir("Missing context.sh")
 
389
 
 
390
    if not context:
 
391
        return results
 
392
 
 
393
    results['metadata'] = context
 
394
 
 
395
    # process single or multiple SSH keys
 
396
    ssh_key_var = None
 
397
    if "SSH_KEY" in context:
 
398
        ssh_key_var = "SSH_KEY"
 
399
    elif "SSH_PUBLIC_KEY" in context:
 
400
        ssh_key_var = "SSH_PUBLIC_KEY"
 
401
 
 
402
    if ssh_key_var:
 
403
        lines = context.get(ssh_key_var).splitlines()
 
404
        results['metadata']['public-keys'] = [l for l in lines
 
405
            if len(l) and not l.startswith("#")]
 
406
 
 
407
    # custom hostname -- try hostname or leave cloud-init
 
408
    # itself create hostname from IP address later
 
409
    for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
 
410
        if k in context:
 
411
            results['metadata']['local-hostname'] = context[k]
 
412
            break
 
413
 
 
414
    # raw user data
 
415
    if "USER_DATA" in context:
 
416
        results['userdata'] = context["USER_DATA"]
 
417
    elif "USERDATA" in context:
 
418
        results['userdata'] = context["USERDATA"]
 
419
 
 
420
    # generate static /etc/network/interfaces
 
421
    # only if there are any required context variables
 
422
    # http://opennebula.org/documentation:rel3.8:cong#network_configuration
 
423
    for k in context.keys():
 
424
        if re.match(r'^ETH\d+_IP$', k):
 
425
            (out, _) = util.subp(['/sbin/ip', 'link'])
 
426
            net = OpenNebulaNetwork(out, context)
 
427
            results['network-interfaces'] = net.gen_conf()
 
428
            break
 
429
 
 
430
    return results
 
431
 
 
432
 
 
433
# Used to match classes to dependencies
 
434
datasources = [
 
435
    (DataSourceOpenNebula, (sources.DEP_FILESYSTEM, )),
 
436
    (DataSourceOpenNebulaNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
 
437
]
 
438
 
 
439
 
 
440
# Return a list of data sources that match this set of dependencies
 
441
def get_datasource_list(depends):
 
442
    return sources.list_from_depends(depends, datasources)