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

« back to all changes in this revision

Viewing changes to .pc/lp-1269626-azure_new_instance.patch/cloudinit/sources/DataSourceAzure.py

  • Committer: Scott Moser
  • Date: 2014-03-21 16:26:05 UTC
  • mfrom: (317.1.2 saucy-proposed.ben)
  • Revision ID: smoser@ubuntu.com-20140321162605-3u4kmqayg5k7agab
* debian/patches/lp-1269626-azure_new_instance.patch:
  fix handling of new instances on Windows Azure (LP: #1269626).
* debian/patches/lp-1292648-azure-format-ephemeral-new.patch:
  re-format ephemeral disk if necessary (LP: #1292648).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vi: ts=4 expandtab
 
2
#
 
3
#    Copyright (C) 2013 Canonical Ltd.
 
4
#
 
5
#    Author: Scott Moser <scott.moser@canonical.com>
 
6
#
 
7
#    This program is free software: you can redistribute it and/or modify
 
8
#    it under the terms of the GNU General Public License version 3, as
 
9
#    published by the Free Software Foundation.
 
10
#
 
11
#    This program is distributed in the hope that it will be useful,
 
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#    GNU General Public License for more details.
 
15
#
 
16
#    You should have received a copy of the GNU General Public License
 
17
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
import base64
 
20
import crypt
 
21
import os
 
22
import os.path
 
23
import time
 
24
from xml.dom import minidom
 
25
 
 
26
from cloudinit import log as logging
 
27
from cloudinit import sources
 
28
from cloudinit import util
 
29
 
 
30
LOG = logging.getLogger(__name__)
 
31
 
 
32
DS_NAME = 'Azure'
 
33
DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"}
 
34
AGENT_START = ['service', 'walinuxagent', 'start']
 
35
BOUNCE_COMMAND = ['sh', '-xc',
 
36
    "i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x"]
 
37
 
 
38
BUILTIN_DS_CONFIG = {
 
39
    'agent_command': AGENT_START,
 
40
    'data_dir': "/var/lib/waagent",
 
41
    'set_hostname': True,
 
42
    'hostname_bounce': {
 
43
        'interface': 'eth0',
 
44
        'policy': True,
 
45
        'command': BOUNCE_COMMAND,
 
46
        'hostname_command': 'hostname',
 
47
        },
 
48
    'disk_aliases': {'ephemeral0': '/dev/sdb'},
 
49
}
 
50
 
 
51
BUILTIN_CLOUD_CONFIG = {
 
52
    'disk_setup': {
 
53
        'ephemeral0': {'table_type': 'mbr',
 
54
                       'layout': True,
 
55
                       'overwrite': False}
 
56
         },
 
57
    'fs_setup': [{'filesystem': 'ext4',
 
58
                  'device': 'ephemeral0.1',
 
59
                  'replace_fs': 'ntfs'}]
 
60
}
 
61
 
 
62
DS_CFG_PATH = ['datasource', DS_NAME]
 
63
 
 
64
 
 
65
class DataSourceAzureNet(sources.DataSource):
 
66
    def __init__(self, sys_cfg, distro, paths):
 
67
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
 
68
        self.seed_dir = os.path.join(paths.seed_dir, 'azure')
 
69
        self.cfg = {}
 
70
        self.seed = None
 
71
        self.ds_cfg = util.mergemanydict([
 
72
            util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
 
73
            BUILTIN_DS_CONFIG])
 
74
 
 
75
    def __str__(self):
 
76
        root = sources.DataSource.__str__(self)
 
77
        return "%s [seed=%s]" % (root, self.seed)
 
78
 
 
79
    def get_data(self):
 
80
        # azure removes/ejects the cdrom containing the ovf-env.xml
 
81
        # file on reboot.  So, in order to successfully reboot we
 
82
        # need to look in the datadir and consider that valid
 
83
        ddir = self.ds_cfg['data_dir']
 
84
 
 
85
        candidates = [self.seed_dir]
 
86
        candidates.extend(list_possible_azure_ds_devs())
 
87
        if ddir:
 
88
            candidates.append(ddir)
 
89
 
 
90
        found = None
 
91
 
 
92
        for cdev in candidates:
 
93
            try:
 
94
                if cdev.startswith("/dev/"):
 
95
                    ret = util.mount_cb(cdev, load_azure_ds_dir)
 
96
                else:
 
97
                    ret = load_azure_ds_dir(cdev)
 
98
 
 
99
            except NonAzureDataSource:
 
100
                continue
 
101
            except BrokenAzureDataSource as exc:
 
102
                raise exc
 
103
            except util.MountFailedError:
 
104
                LOG.warn("%s was not mountable" % cdev)
 
105
                continue
 
106
 
 
107
            (md, self.userdata_raw, cfg, files) = ret
 
108
            self.seed = cdev
 
109
            self.metadata = util.mergemanydict([md, DEFAULT_METADATA])
 
110
            self.cfg = util.mergemanydict([cfg, BUILTIN_CLOUD_CONFIG])
 
111
            found = cdev
 
112
 
 
113
            LOG.debug("found datasource in %s", cdev)
 
114
            break
 
115
 
 
116
        if not found:
 
117
            return False
 
118
 
 
119
        if found == ddir:
 
120
            LOG.debug("using files cached in %s", ddir)
 
121
 
 
122
        # azure / hyper-v provides random data here
 
123
        seed = util.load_file("/sys/firmware/acpi/tables/OEM0", quiet=True)
 
124
        if seed:
 
125
            self.metadata['random_seed'] = seed
 
126
 
 
127
        # now update ds_cfg to reflect contents pass in config
 
128
        user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
 
129
        self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
 
130
        mycfg = self.ds_cfg
 
131
 
 
132
        # walinux agent writes files world readable, but expects
 
133
        # the directory to be protected.
 
134
        write_files(mycfg['data_dir'], files, dirmode=0700)
 
135
 
 
136
        # handle the hostname 'publishing'
 
137
        try:
 
138
            handle_set_hostname(mycfg.get('set_hostname'),
 
139
                                self.metadata.get('local-hostname'),
 
140
                                mycfg['hostname_bounce'])
 
141
        except Exception as e:
 
142
            LOG.warn("Failed publishing hostname: %s" % e)
 
143
            util.logexc(LOG, "handling set_hostname failed")
 
144
 
 
145
        try:
 
146
            invoke_agent(mycfg['agent_command'])
 
147
        except util.ProcessExecutionError:
 
148
            # claim the datasource even if the command failed
 
149
            util.logexc(LOG, "agent command '%s' failed.",
 
150
                        mycfg['agent_command'])
 
151
 
 
152
        shcfgxml = os.path.join(mycfg['data_dir'], "SharedConfig.xml")
 
153
        wait_for = [shcfgxml]
 
154
 
 
155
        fp_files = []
 
156
        for pk in self.cfg.get('_pubkeys', []):
 
157
            bname = pk['fingerprint'] + ".crt"
 
158
            fp_files += [os.path.join(mycfg['data_dir'], bname)]
 
159
 
 
160
        missing = util.log_time(logfunc=LOG.debug, msg="waiting for files",
 
161
                                func=wait_for_files,
 
162
                                args=(wait_for + fp_files,))
 
163
        if len(missing):
 
164
            LOG.warn("Did not find files, but going on: %s", missing)
 
165
 
 
166
        if shcfgxml in missing:
 
167
            LOG.warn("SharedConfig.xml missing, using static instance-id")
 
168
        else:
 
169
            try:
 
170
                self.metadata['instance-id'] = iid_from_shared_config(shcfgxml)
 
171
            except ValueError as e:
 
172
                LOG.warn("failed to get instance id in %s: %s" % (shcfgxml, e))
 
173
 
 
174
        pubkeys = pubkeys_from_crt_files(fp_files)
 
175
 
 
176
        self.metadata['public-keys'] = pubkeys
 
177
        return True
 
178
 
 
179
    def device_name_to_device(self, name):
 
180
        return self.ds_cfg['disk_aliases'].get(name)
 
181
 
 
182
    def get_config_obj(self):
 
183
        return self.cfg
 
184
 
 
185
 
 
186
def handle_set_hostname(enabled, hostname, cfg):
 
187
    if not util.is_true(enabled):
 
188
        return
 
189
 
 
190
    if not hostname:
 
191
        LOG.warn("set_hostname was true but no local-hostname")
 
192
        return
 
193
 
 
194
    apply_hostname_bounce(hostname=hostname, policy=cfg['policy'],
 
195
                          interface=cfg['interface'],
 
196
                          command=cfg['command'],
 
197
                          hostname_command=cfg['hostname_command'])
 
198
 
 
199
 
 
200
def apply_hostname_bounce(hostname, policy, interface, command,
 
201
                          hostname_command="hostname"):
 
202
    # set the hostname to 'hostname' if it is not already set to that.
 
203
    # then, if policy is not off, bounce the interface using command
 
204
    prev_hostname = util.subp(hostname_command, capture=True)[0].strip()
 
205
 
 
206
    util.subp([hostname_command, hostname])
 
207
 
 
208
    msg = ("phostname=%s hostname=%s policy=%s interface=%s" %
 
209
           (prev_hostname, hostname, policy, interface))
 
210
 
 
211
    if util.is_false(policy):
 
212
        LOG.debug("pubhname: policy false, skipping [%s]", msg)
 
213
        return
 
214
 
 
215
    if prev_hostname == hostname and policy != "force":
 
216
        LOG.debug("pubhname: no change, policy != force. skipping. [%s]", msg)
 
217
        return
 
218
 
 
219
    env = os.environ.copy()
 
220
    env['interface'] = interface
 
221
    env['hostname'] = hostname
 
222
    env['old_hostname'] = prev_hostname
 
223
 
 
224
    if command == "builtin":
 
225
        command = BOUNCE_COMMAND
 
226
 
 
227
    LOG.debug("pubhname: publishing hostname [%s]", msg)
 
228
    shell = not isinstance(command, (list, tuple))
 
229
    # capture=False, see comments in bug 1202758 and bug 1206164.
 
230
    util.log_time(logfunc=LOG.debug, msg="publishing hostname",
 
231
        get_uptime=True, func=util.subp,
 
232
        kwargs={'args': command, 'shell': shell, 'capture': False,
 
233
                'env': env})
 
234
 
 
235
 
 
236
def crtfile_to_pubkey(fname):
 
237
    pipeline = ('openssl x509 -noout -pubkey < "$0" |'
 
238
                'ssh-keygen -i -m PKCS8 -f /dev/stdin')
 
239
    (out, _err) = util.subp(['sh', '-c', pipeline, fname], capture=True)
 
240
    return out.rstrip()
 
241
 
 
242
 
 
243
def pubkeys_from_crt_files(flist):
 
244
    pubkeys = []
 
245
    errors = []
 
246
    for fname in flist:
 
247
        try:
 
248
            pubkeys.append(crtfile_to_pubkey(fname))
 
249
        except util.ProcessExecutionError:
 
250
            errors.extend(fname)
 
251
 
 
252
    if errors:
 
253
        LOG.warn("failed to convert the crt files to pubkey: %s" % errors)
 
254
 
 
255
    return pubkeys
 
256
 
 
257
 
 
258
def wait_for_files(flist, maxwait=60, naplen=.5):
 
259
    need = set(flist)
 
260
    waited = 0
 
261
    while waited < maxwait:
 
262
        need -= set([f for f in need if os.path.exists(f)])
 
263
        if len(need) == 0:
 
264
            return []
 
265
        time.sleep(naplen)
 
266
        waited += naplen
 
267
    return need
 
268
 
 
269
 
 
270
def write_files(datadir, files, dirmode=None):
 
271
    if not datadir:
 
272
        return
 
273
    if not files:
 
274
        files = {}
 
275
    util.ensure_dir(datadir, dirmode)
 
276
    for (name, content) in files.items():
 
277
        util.write_file(filename=os.path.join(datadir, name),
 
278
                        content=content, mode=0600)
 
279
 
 
280
 
 
281
def invoke_agent(cmd):
 
282
    # this is a function itself to simplify patching it for test
 
283
    if cmd:
 
284
        LOG.debug("invoking agent: %s" % cmd)
 
285
        util.subp(cmd, shell=(not isinstance(cmd, list)))
 
286
    else:
 
287
        LOG.debug("not invoking agent")
 
288
 
 
289
 
 
290
def find_child(node, filter_func):
 
291
    ret = []
 
292
    if not node.hasChildNodes():
 
293
        return ret
 
294
    for child in node.childNodes:
 
295
        if filter_func(child):
 
296
            ret.append(child)
 
297
    return ret
 
298
 
 
299
 
 
300
def load_azure_ovf_pubkeys(sshnode):
 
301
    # This parses a 'SSH' node formatted like below, and returns
 
302
    # an array of dicts.
 
303
    #  [{'fp': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
 
304
    #    'path': 'where/to/go'}]
 
305
    #
 
306
    # <SSH><PublicKeys>
 
307
    #   <PublicKey><Fingerprint>ABC</FingerPrint><Path>/ABC</Path>
 
308
    #   ...
 
309
    # </PublicKeys></SSH>
 
310
    results = find_child(sshnode, lambda n: n.localName == "PublicKeys")
 
311
    if len(results) == 0:
 
312
        return []
 
313
    if len(results) > 1:
 
314
        raise BrokenAzureDataSource("Multiple 'PublicKeys'(%s) in SSH node" %
 
315
                                    len(results))
 
316
 
 
317
    pubkeys_node = results[0]
 
318
    pubkeys = find_child(pubkeys_node, lambda n: n.localName == "PublicKey")
 
319
 
 
320
    if len(pubkeys) == 0:
 
321
        return []
 
322
 
 
323
    found = []
 
324
    text_node = minidom.Document.TEXT_NODE
 
325
 
 
326
    for pk_node in pubkeys:
 
327
        if not pk_node.hasChildNodes():
 
328
            continue
 
329
        cur = {'fingerprint': "", 'path': ""}
 
330
        for child in pk_node.childNodes:
 
331
            if (child.nodeType == text_node or not child.localName):
 
332
                continue
 
333
 
 
334
            name = child.localName.lower()
 
335
 
 
336
            if name not in cur.keys():
 
337
                continue
 
338
 
 
339
            if (len(child.childNodes) != 1 or
 
340
                child.childNodes[0].nodeType != text_node):
 
341
                continue
 
342
 
 
343
            cur[name] = child.childNodes[0].wholeText.strip()
 
344
        found.append(cur)
 
345
 
 
346
    return found
 
347
 
 
348
 
 
349
def single_node_at_path(node, pathlist):
 
350
    curnode = node
 
351
    for tok in pathlist:
 
352
        results = find_child(curnode, lambda n: n.localName == tok)
 
353
        if len(results) == 0:
 
354
            raise ValueError("missing %s token in %s" % (tok, str(pathlist)))
 
355
        if len(results) > 1:
 
356
            raise ValueError("found %s nodes of type %s looking for %s" %
 
357
                             (len(results), tok, str(pathlist)))
 
358
        curnode = results[0]
 
359
 
 
360
    return curnode
 
361
 
 
362
 
 
363
def read_azure_ovf(contents):
 
364
    try:
 
365
        dom = minidom.parseString(contents)
 
366
    except Exception as e:
 
367
        raise BrokenAzureDataSource("invalid xml: %s" % e)
 
368
 
 
369
    results = find_child(dom.documentElement,
 
370
        lambda n: n.localName == "ProvisioningSection")
 
371
 
 
372
    if len(results) == 0:
 
373
        raise NonAzureDataSource("No ProvisioningSection")
 
374
    if len(results) > 1:
 
375
        raise BrokenAzureDataSource("found '%d' ProvisioningSection items" %
 
376
                                    len(results))
 
377
    provSection = results[0]
 
378
 
 
379
    lpcs_nodes = find_child(provSection,
 
380
        lambda n: n.localName == "LinuxProvisioningConfigurationSet")
 
381
 
 
382
    if len(results) == 0:
 
383
        raise NonAzureDataSource("No LinuxProvisioningConfigurationSet")
 
384
    if len(results) > 1:
 
385
        raise BrokenAzureDataSource("found '%d' %ss" %
 
386
                                    ("LinuxProvisioningConfigurationSet",
 
387
                                     len(results)))
 
388
    lpcs = lpcs_nodes[0]
 
389
 
 
390
    if not lpcs.hasChildNodes():
 
391
        raise BrokenAzureDataSource("no child nodes of configuration set")
 
392
 
 
393
    md_props = 'seedfrom'
 
394
    md = {'azure_data': {}}
 
395
    cfg = {}
 
396
    ud = ""
 
397
    password = None
 
398
    username = None
 
399
 
 
400
    for child in lpcs.childNodes:
 
401
        if child.nodeType == dom.TEXT_NODE or not child.localName:
 
402
            continue
 
403
 
 
404
        name = child.localName.lower()
 
405
 
 
406
        simple = False
 
407
        value = ""
 
408
        if (len(child.childNodes) == 1 and
 
409
            child.childNodes[0].nodeType == dom.TEXT_NODE):
 
410
            simple = True
 
411
            value = child.childNodes[0].wholeText
 
412
 
 
413
        attrs = {k: v for k, v in child.attributes.items()}
 
414
 
 
415
        # we accept either UserData or CustomData.  If both are present
 
416
        # then behavior is undefined.
 
417
        if (name == "userdata" or name == "customdata"):
 
418
            if attrs.get('encoding') in (None, "base64"):
 
419
                ud = base64.b64decode(''.join(value.split()))
 
420
            else:
 
421
                ud = value
 
422
        elif name == "username":
 
423
            username = value
 
424
        elif name == "userpassword":
 
425
            password = value
 
426
        elif name == "hostname":
 
427
            md['local-hostname'] = value
 
428
        elif name == "dscfg":
 
429
            if attrs.get('encoding') in (None, "base64"):
 
430
                dscfg = base64.b64decode(''.join(value.split()))
 
431
            else:
 
432
                dscfg = value
 
433
            cfg['datasource'] = {DS_NAME: util.load_yaml(dscfg, default={})}
 
434
        elif name == "ssh":
 
435
            cfg['_pubkeys'] = load_azure_ovf_pubkeys(child)
 
436
        elif name == "disablesshpasswordauthentication":
 
437
            cfg['ssh_pwauth'] = util.is_false(value)
 
438
        elif simple:
 
439
            if name in md_props:
 
440
                md[name] = value
 
441
            else:
 
442
                md['azure_data'][name] = value
 
443
 
 
444
    defuser = {}
 
445
    if username:
 
446
        defuser['name'] = username
 
447
    if password:
 
448
        defuser['passwd'] = encrypt_pass(password)
 
449
        defuser['lock_passwd'] = False
 
450
 
 
451
    if defuser:
 
452
        cfg['system_info'] = {'default_user': defuser}
 
453
 
 
454
    if 'ssh_pwauth' not in cfg and password:
 
455
        cfg['ssh_pwauth'] = True
 
456
 
 
457
    return (md, ud, cfg)
 
458
 
 
459
 
 
460
def encrypt_pass(password, salt_id="$6$"):
 
461
    return crypt.crypt(password, salt_id + util.rand_str(strlen=16))
 
462
 
 
463
 
 
464
def list_possible_azure_ds_devs():
 
465
    # return a sorted list of devices that might have a azure datasource
 
466
    devlist = []
 
467
    for fstype in ("iso9660", "udf"):
 
468
        devlist.extend(util.find_devs_with("TYPE=%s" % fstype))
 
469
 
 
470
    devlist.sort(reverse=True)
 
471
    return devlist
 
472
 
 
473
 
 
474
def load_azure_ds_dir(source_dir):
 
475
    ovf_file = os.path.join(source_dir, "ovf-env.xml")
 
476
 
 
477
    if not os.path.isfile(ovf_file):
 
478
        raise NonAzureDataSource("No ovf-env file found")
 
479
 
 
480
    with open(ovf_file, "r") as fp:
 
481
        contents = fp.read()
 
482
 
 
483
    md, ud, cfg = read_azure_ovf(contents)
 
484
    return (md, ud, cfg, {'ovf-env.xml': contents})
 
485
 
 
486
 
 
487
def iid_from_shared_config(path):
 
488
    with open(path, "rb") as fp:
 
489
        content = fp.read()
 
490
    return iid_from_shared_config_content(content)
 
491
 
 
492
 
 
493
def iid_from_shared_config_content(content):
 
494
    """
 
495
    find INSTANCE_ID in:
 
496
    <?xml version="1.0" encoding="utf-8"?>
 
497
    <SharedConfig version="1.0.0.0" goalStateIncarnation="1">
 
498
      <Deployment name="INSTANCE_ID" guid="{...}" incarnation="0">
 
499
        <Service name="..." guid="{00000000-0000-0000-0000-000000000000}" />
 
500
    """
 
501
    dom = minidom.parseString(content)
 
502
    depnode = single_node_at_path(dom, ["SharedConfig", "Deployment"])
 
503
    return depnode.attributes.get('name').value
 
504
 
 
505
 
 
506
class BrokenAzureDataSource(Exception):
 
507
    pass
 
508
 
 
509
 
 
510
class NonAzureDataSource(Exception):
 
511
    pass
 
512
 
 
513
 
 
514
# Used to match classes to dependencies
 
515
datasources = [
 
516
  (DataSourceAzureNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
 
517
]
 
518
 
 
519
 
 
520
# Return a list of data sources that match this set of dependencies
 
521
def get_datasource_list(depends):
 
522
    return sources.list_from_depends(depends, datasources)