~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/sources/helpers/openstack.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

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
 
#
6
 
#    Author: Scott Moser <scott.moser@canonical.com>
7
 
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
8
 
#
9
 
#    This program is free software: you can redistribute it and/or modify
10
 
#    it under the terms of the GNU General Public License version 3, as
11
 
#    published by the Free Software Foundation.
12
 
#
13
 
#    This program is distributed in the hope that it will be useful,
14
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 
#    GNU General Public License for more details.
17
 
#
18
 
#    You should have received a copy of the GNU General Public License
19
 
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 
 
21
 
import abc
22
 
import base64
23
 
import copy
24
 
import functools
25
 
import os
26
 
 
27
 
import six
28
 
 
29
 
from cloudinit import ec2_utils
30
 
from cloudinit import log as logging
31
 
from cloudinit import net
32
 
from cloudinit import sources
33
 
from cloudinit import url_helper
34
 
from cloudinit import util
35
 
 
36
 
# For reference: http://tinyurl.com/laora4c
37
 
 
38
 
LOG = logging.getLogger(__name__)
39
 
 
40
 
FILES_V1 = {
41
 
    # Path <-> (metadata key name, translator function, default value)
42
 
    'etc/network/interfaces': ('network_config', lambda x: x, ''),
43
 
    'meta.js': ('meta_js', util.load_json, {}),
44
 
    "root/.ssh/authorized_keys": ('authorized_keys', lambda x: x, ''),
45
 
}
46
 
KEY_COPIES = (
47
 
    # Cloud-init metadata names <-> (metadata key, is required)
48
 
    ('local-hostname', 'hostname', False),
49
 
    ('instance-id', 'uuid', True),
50
 
)
51
 
OS_LATEST = 'latest'
52
 
OS_FOLSOM = '2012-08-10'
53
 
OS_GRIZZLY = '2013-04-04'
54
 
OS_HAVANA = '2013-10-17'
55
 
OS_LIBERTY = '2015-10-15'
56
 
# keep this in chronological order. new supported versions go at the end.
57
 
OS_VERSIONS = (
58
 
    OS_FOLSOM,
59
 
    OS_GRIZZLY,
60
 
    OS_HAVANA,
61
 
    OS_LIBERTY,
62
 
)
63
 
 
64
 
 
65
 
class NonReadable(IOError):
66
 
    pass
67
 
 
68
 
 
69
 
class BrokenMetadata(IOError):
70
 
    pass
71
 
 
72
 
 
73
 
class SourceMixin(object):
74
 
    def _ec2_name_to_device(self, name):
75
 
        if not self.ec2_metadata:
76
 
            return None
77
 
        bdm = self.ec2_metadata.get('block-device-mapping', {})
78
 
        for (ent_name, device) in bdm.items():
79
 
            if name == ent_name:
80
 
                return device
81
 
        return None
82
 
 
83
 
    def get_public_ssh_keys(self):
84
 
        name = "public_keys"
85
 
        if self.version == 1:
86
 
            name = "public-keys"
87
 
        return sources.normalize_pubkey_data(self.metadata.get(name))
88
 
 
89
 
    def _os_name_to_device(self, name):
90
 
        device = None
91
 
        try:
92
 
            criteria = 'LABEL=%s' % (name)
93
 
            if name == 'swap':
94
 
                criteria = 'TYPE=%s' % (name)
95
 
            dev_entries = util.find_devs_with(criteria)
96
 
            if dev_entries:
97
 
                device = dev_entries[0]
98
 
        except util.ProcessExecutionError:
99
 
            pass
100
 
        return device
101
 
 
102
 
    def _validate_device_name(self, device):
103
 
        if not device:
104
 
            return None
105
 
        if not device.startswith("/"):
106
 
            device = "/dev/%s" % device
107
 
        if os.path.exists(device):
108
 
            return device
109
 
        # Durn, try adjusting the mapping
110
 
        remapped = self._remap_device(os.path.basename(device))
111
 
        if remapped:
112
 
            LOG.debug("Remapped device name %s => %s", device, remapped)
113
 
            return remapped
114
 
        return None
115
 
 
116
 
    def device_name_to_device(self, name):
117
 
        # Translate a 'name' to a 'physical' device
118
 
        if not name:
119
 
            return None
120
 
        # Try the ec2 mapping first
121
 
        names = [name]
122
 
        if name == 'root':
123
 
            names.insert(0, 'ami')
124
 
        if name == 'ami':
125
 
            names.append('root')
126
 
        device = None
127
 
        LOG.debug("Using ec2 style lookup to find device %s", names)
128
 
        for n in names:
129
 
            device = self._ec2_name_to_device(n)
130
 
            device = self._validate_device_name(device)
131
 
            if device:
132
 
                break
133
 
        # Try the openstack way second
134
 
        if not device:
135
 
            LOG.debug("Using openstack style lookup to find device %s", names)
136
 
            for n in names:
137
 
                device = self._os_name_to_device(n)
138
 
                device = self._validate_device_name(device)
139
 
                if device:
140
 
                    break
141
 
        # Ok give up...
142
 
        if not device:
143
 
            return None
144
 
        else:
145
 
            LOG.debug("Mapped %s to device %s", name, device)
146
 
            return device
147
 
 
148
 
 
149
 
@six.add_metaclass(abc.ABCMeta)
150
 
class BaseReader(object):
151
 
 
152
 
    def __init__(self, base_path):
153
 
        self.base_path = base_path
154
 
 
155
 
    @abc.abstractmethod
156
 
    def _path_join(self, base, *add_ons):
157
 
        pass
158
 
 
159
 
    @abc.abstractmethod
160
 
    def _path_read(self, path, decode=False):
161
 
        pass
162
 
 
163
 
    @abc.abstractmethod
164
 
    def _fetch_available_versions(self):
165
 
        pass
166
 
 
167
 
    @abc.abstractmethod
168
 
    def _read_ec2_metadata(self):
169
 
        pass
170
 
 
171
 
    def _find_working_version(self):
172
 
        try:
173
 
            versions_available = self._fetch_available_versions()
174
 
        except Exception as e:
175
 
            LOG.debug("Unable to read openstack versions from %s due to: %s",
176
 
                      self.base_path, e)
177
 
            versions_available = []
178
 
 
179
 
        # openstack.OS_VERSIONS is stored in chronological order, so
180
 
        # reverse it to check newest first.
181
 
        supported = [v for v in reversed(list(OS_VERSIONS))]
182
 
        selected_version = OS_LATEST
183
 
 
184
 
        for potential_version in supported:
185
 
            if potential_version not in versions_available:
186
 
                continue
187
 
            selected_version = potential_version
188
 
            break
189
 
 
190
 
        LOG.debug("Selected version '%s' from %s", selected_version,
191
 
                  versions_available)
192
 
        return selected_version
193
 
 
194
 
    def _read_content_path(self, item, decode=False):
195
 
        path = item.get('content_path', '').lstrip("/")
196
 
        path_pieces = path.split("/")
197
 
        valid_pieces = [p for p in path_pieces if len(p)]
198
 
        if not valid_pieces:
199
 
            raise BrokenMetadata("Item %s has no valid content path" % (item))
200
 
        path = self._path_join(self.base_path, "openstack", *path_pieces)
201
 
        return self._path_read(path, decode=decode)
202
 
 
203
 
    def read_v2(self):
204
 
        """Reads a version 2 formatted location.
205
 
 
206
 
        Return a dict with metadata, userdata, ec2-metadata, dsmode,
207
 
        network_config, files and version (2).
208
 
 
209
 
        If not a valid location, raise a NonReadable exception.
210
 
        """
211
 
 
212
 
        load_json_anytype = functools.partial(
213
 
            util.load_json, root_types=(dict, list) + six.string_types)
214
 
 
215
 
        def datafiles(version):
216
 
            files = {}
217
 
            files['metadata'] = (
218
 
                # File path to read
219
 
                self._path_join("openstack", version, 'meta_data.json'),
220
 
                # Is it required?
221
 
                True,
222
 
                # Translator function (applied after loading)
223
 
                util.load_json,
224
 
            )
225
 
            files['userdata'] = (
226
 
                self._path_join("openstack", version, 'user_data'),
227
 
                False,
228
 
                lambda x: x,
229
 
            )
230
 
            files['vendordata'] = (
231
 
                self._path_join("openstack", version, 'vendor_data.json'),
232
 
                False,
233
 
                load_json_anytype,
234
 
            )
235
 
            files['networkdata'] = (
236
 
                self._path_join("openstack", version, 'network_data.json'),
237
 
                False,
238
 
                load_json_anytype,
239
 
            )
240
 
            return files
241
 
 
242
 
        results = {
243
 
            'userdata': '',
244
 
            'version': 2,
245
 
        }
246
 
        data = datafiles(self._find_working_version())
247
 
        for (name, (path, required, translator)) in data.items():
248
 
            path = self._path_join(self.base_path, path)
249
 
            data = None
250
 
            found = False
251
 
            try:
252
 
                data = self._path_read(path)
253
 
            except IOError as e:
254
 
                if not required:
255
 
                    LOG.debug("Failed reading optional path %s due"
256
 
                              " to: %s", path, e)
257
 
                else:
258
 
                    LOG.debug("Failed reading mandatory path %s due"
259
 
                              " to: %s", path, e)
260
 
            else:
261
 
                found = True
262
 
            if required and not found:
263
 
                raise NonReadable("Missing mandatory path: %s" % path)
264
 
            if found and translator:
265
 
                try:
266
 
                    data = translator(data)
267
 
                except Exception as e:
268
 
                    raise BrokenMetadata("Failed to process "
269
 
                                         "path %s: %s" % (path, e))
270
 
            if found:
271
 
                results[name] = data
272
 
 
273
 
        metadata = results['metadata']
274
 
        if 'random_seed' in metadata:
275
 
            random_seed = metadata['random_seed']
276
 
            try:
277
 
                metadata['random_seed'] = base64.b64decode(random_seed)
278
 
            except (ValueError, TypeError) as e:
279
 
                raise BrokenMetadata("Badly formatted metadata"
280
 
                                     " random_seed entry: %s" % e)
281
 
 
282
 
        # load any files that were provided
283
 
        files = {}
284
 
        metadata_files = metadata.get('files', [])
285
 
        for item in metadata_files:
286
 
            if 'path' not in item:
287
 
                continue
288
 
            path = item['path']
289
 
            try:
290
 
                files[path] = self._read_content_path(item)
291
 
            except Exception as e:
292
 
                raise BrokenMetadata("Failed to read provided "
293
 
                                     "file %s: %s" % (path, e))
294
 
        results['files'] = files
295
 
 
296
 
        # The 'network_config' item in metadata is a content pointer
297
 
        # to the network config that should be applied. It is just a
298
 
        # ubuntu/debian '/etc/network/interfaces' file.
299
 
        net_item = metadata.get("network_config", None)
300
 
        if net_item:
301
 
            try:
302
 
                content = self._read_content_path(net_item, decode=True)
303
 
                results['network_config'] = content
304
 
            except IOError as e:
305
 
                raise BrokenMetadata("Failed to read network"
306
 
                                     " configuration: %s" % (e))
307
 
 
308
 
        # To openstack, user can specify meta ('nova boot --meta=key=value')
309
 
        # and those will appear under metadata['meta'].
310
 
        # if they specify 'dsmode' they're indicating the mode that they intend
311
 
        # for this datasource to operate in.
312
 
        try:
313
 
            results['dsmode'] = metadata['meta']['dsmode']
314
 
        except KeyError:
315
 
            pass
316
 
 
317
 
        # Read any ec2-metadata (if applicable)
318
 
        results['ec2-metadata'] = self._read_ec2_metadata()
319
 
 
320
 
        # Perform some misc. metadata key renames...
321
 
        for (target_key, source_key, is_required) in KEY_COPIES:
322
 
            if is_required and source_key not in metadata:
323
 
                raise BrokenMetadata("No '%s' entry in metadata" % source_key)
324
 
            if source_key in metadata:
325
 
                metadata[target_key] = metadata.get(source_key)
326
 
        return results
327
 
 
328
 
 
329
 
class ConfigDriveReader(BaseReader):
330
 
    def __init__(self, base_path):
331
 
        super(ConfigDriveReader, self).__init__(base_path)
332
 
        self._versions = None
333
 
 
334
 
    def _path_join(self, base, *add_ons):
335
 
        components = [base] + list(add_ons)
336
 
        return os.path.join(*components)
337
 
 
338
 
    def _path_read(self, path, decode=False):
339
 
        return util.load_file(path, decode=decode)
340
 
 
341
 
    def _fetch_available_versions(self):
342
 
        if self._versions is None:
343
 
            path = self._path_join(self.base_path, 'openstack')
344
 
            found = [d for d in os.listdir(path)
345
 
                     if os.path.isdir(os.path.join(path))]
346
 
            self._versions = sorted(found)
347
 
        return self._versions
348
 
 
349
 
    def _read_ec2_metadata(self):
350
 
        path = self._path_join(self.base_path,
351
 
                               'ec2', 'latest', 'meta-data.json')
352
 
        if not os.path.exists(path):
353
 
            return {}
354
 
        else:
355
 
            try:
356
 
                return util.load_json(self._path_read(path))
357
 
            except Exception as e:
358
 
                raise BrokenMetadata("Failed to process "
359
 
                                     "path %s: %s" % (path, e))
360
 
 
361
 
    def read_v1(self):
362
 
        """Reads a version 1 formatted location.
363
 
 
364
 
        Return a dict with metadata, userdata, dsmode, files and version (1).
365
 
 
366
 
        If not a valid path, raise a NonReadable exception.
367
 
        """
368
 
 
369
 
        found = {}
370
 
        for name in FILES_V1.keys():
371
 
            path = self._path_join(self.base_path, name)
372
 
            if os.path.exists(path):
373
 
                found[name] = path
374
 
        if len(found) == 0:
375
 
            raise NonReadable("%s: no files found" % (self.base_path))
376
 
 
377
 
        md = {}
378
 
        for (name, (key, translator, default)) in FILES_V1.items():
379
 
            if name in found:
380
 
                path = found[name]
381
 
                try:
382
 
                    contents = self._path_read(path)
383
 
                except IOError:
384
 
                    raise BrokenMetadata("Failed to read: %s" % path)
385
 
                try:
386
 
                    md[key] = translator(contents)
387
 
                except Exception as e:
388
 
                    raise BrokenMetadata("Failed to process "
389
 
                                         "path %s: %s" % (path, e))
390
 
            else:
391
 
                md[key] = copy.deepcopy(default)
392
 
 
393
 
        keydata = md['authorized_keys']
394
 
        meta_js = md['meta_js']
395
 
 
396
 
        # keydata in meta_js is preferred over "injected"
397
 
        keydata = meta_js.get('public-keys', keydata)
398
 
        if keydata:
399
 
            lines = keydata.splitlines()
400
 
            md['public-keys'] = [l for l in lines
401
 
                                 if len(l) and not l.startswith("#")]
402
 
 
403
 
        # config-drive-v1 has no way for openstack to provide the instance-id
404
 
        # so we copy that into metadata from the user input
405
 
        if 'instance-id' in meta_js:
406
 
            md['instance-id'] = meta_js['instance-id']
407
 
 
408
 
        results = {
409
 
            'version': 1,
410
 
            'metadata': md,
411
 
        }
412
 
 
413
 
        # allow the user to specify 'dsmode' in a meta tag
414
 
        if 'dsmode' in meta_js:
415
 
            results['dsmode'] = meta_js['dsmode']
416
 
 
417
 
        # config-drive-v1 has no way of specifying user-data, so the user has
418
 
        # to cheat and stuff it in a meta tag also.
419
 
        results['userdata'] = meta_js.get('user-data', '')
420
 
 
421
 
        # this implementation does not support files other than
422
 
        # network/interfaces and authorized_keys...
423
 
        results['files'] = {}
424
 
 
425
 
        return results
426
 
 
427
 
 
428
 
class MetadataReader(BaseReader):
429
 
    def __init__(self, base_url, ssl_details=None, timeout=5, retries=5):
430
 
        super(MetadataReader, self).__init__(base_url)
431
 
        self.ssl_details = ssl_details
432
 
        self.timeout = float(timeout)
433
 
        self.retries = int(retries)
434
 
        self._versions = None
435
 
 
436
 
    def _fetch_available_versions(self):
437
 
        # <baseurl>/openstack/ returns a newline separated list of versions
438
 
        if self._versions is not None:
439
 
            return self._versions
440
 
        found = []
441
 
        version_path = self._path_join(self.base_path, "openstack")
442
 
        content = self._path_read(version_path)
443
 
        for line in content.splitlines():
444
 
            line = line.strip()
445
 
            if not line:
446
 
                continue
447
 
            found.append(line)
448
 
        self._versions = found
449
 
        return self._versions
450
 
 
451
 
    def _path_read(self, path, decode=False):
452
 
 
453
 
        def should_retry_cb(_request_args, cause):
454
 
            try:
455
 
                code = int(cause.code)
456
 
                if code >= 400:
457
 
                    return False
458
 
            except (TypeError, ValueError):
459
 
                # Older versions of requests didn't have a code.
460
 
                pass
461
 
            return True
462
 
 
463
 
        response = url_helper.readurl(path,
464
 
                                      retries=self.retries,
465
 
                                      ssl_details=self.ssl_details,
466
 
                                      timeout=self.timeout,
467
 
                                      exception_cb=should_retry_cb)
468
 
        if decode:
469
 
            return response.contents.decode()
470
 
        else:
471
 
            return response.contents
472
 
 
473
 
    def _path_join(self, base, *add_ons):
474
 
        return url_helper.combine_url(base, *add_ons)
475
 
 
476
 
    def _read_ec2_metadata(self):
477
 
        return ec2_utils.get_instance_metadata(ssl_details=self.ssl_details,
478
 
                                               timeout=self.timeout,
479
 
                                               retries=self.retries)
480
 
 
481
 
 
482
 
# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
483
 
def convert_net_json(network_json=None, known_macs=None):
484
 
    """Return a dictionary of network_config by parsing provided
485
 
       OpenStack ConfigDrive NetworkData json format
486
 
 
487
 
    OpenStack network_data.json provides a 3 element dictionary
488
 
      - "links" (links are network devices, physical or virtual)
489
 
      - "networks" (networks are ip network configurations for one or more
490
 
                    links)
491
 
      -  services (non-ip services, like dns)
492
 
 
493
 
    networks and links are combined via network items referencing specific
494
 
    links via a 'link_id' which maps to a links 'id' field.
495
 
 
496
 
    To convert this format to network_config yaml, we first iterate over the
497
 
    links and then walk the network list to determine if any of the networks
498
 
    utilize the current link; if so we generate a subnet entry for the device
499
 
 
500
 
    We also need to map network_data.json fields to network_config fields. For
501
 
    example, the network_data links 'id' field is equivalent to network_config
502
 
    'name' field for devices.  We apply more of this mapping to the various
503
 
    link types that we encounter.
504
 
 
505
 
    There are additional fields that are populated in the network_data.json
506
 
    from OpenStack that are not relevant to network_config yaml, so we
507
 
    enumerate a dictionary of valid keys for network_yaml and apply filtering
508
 
    to drop these superflous keys from the network_config yaml.
509
 
    """
510
 
    if network_json is None:
511
 
        return None
512
 
 
513
 
    # dict of network_config key for filtering network_json
514
 
    valid_keys = {
515
 
        'physical': [
516
 
            'name',
517
 
            'type',
518
 
            'mac_address',
519
 
            'subnets',
520
 
            'params',
521
 
            'mtu',
522
 
        ],
523
 
        'subnet': [
524
 
            'type',
525
 
            'address',
526
 
            'netmask',
527
 
            'broadcast',
528
 
            'metric',
529
 
            'gateway',
530
 
            'pointopoint',
531
 
            'scope',
532
 
            'dns_nameservers',
533
 
            'dns_search',
534
 
            'routes',
535
 
        ],
536
 
    }
537
 
 
538
 
    links = network_json.get('links', [])
539
 
    networks = network_json.get('networks', [])
540
 
    services = network_json.get('services', [])
541
 
 
542
 
    config = []
543
 
    for link in links:
544
 
        subnets = []
545
 
        cfg = dict((k, v) for k, v in link.items()
546
 
                   if k in valid_keys['physical'])
547
 
        # 'name' is not in openstack spec yet, but we will support it if it is
548
 
        # present.  The 'id' in the spec is currently implemented as the host
549
 
        # nic's name, meaning something like 'tap-adfasdffd'.  We do not want
550
 
        # to name guest devices with such ugly names.
551
 
        if 'name' in link:
552
 
            cfg['name'] = link['name']
553
 
 
554
 
        for network in [n for n in networks
555
 
                        if n['link'] == link['id']]:
556
 
            subnet = dict((k, v) for k, v in network.items()
557
 
                          if k in valid_keys['subnet'])
558
 
            if 'dhcp' in network['type']:
559
 
                t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
560
 
                subnet.update({
561
 
                    'type': t,
562
 
                })
563
 
            else:
564
 
                subnet.update({
565
 
                    'type': 'static',
566
 
                    'address': network.get('ip_address'),
567
 
                })
568
 
            if network['type'] == 'ipv4':
569
 
                subnet['ipv4'] = True
570
 
            if network['type'] == 'ipv6':
571
 
                subnet['ipv6'] = True
572
 
            subnets.append(subnet)
573
 
        cfg.update({'subnets': subnets})
574
 
        if link['type'] in ['ethernet', 'vif', 'ovs', 'phy', 'bridge']:
575
 
            cfg.update({
576
 
                'type': 'physical',
577
 
                'mac_address': link['ethernet_mac_address']})
578
 
        elif link['type'] in ['bond']:
579
 
            params = {}
580
 
            for k, v in link.items():
581
 
                if k == 'bond_links':
582
 
                    continue
583
 
                elif k.startswith('bond'):
584
 
                    params.update({k: v})
585
 
            cfg.update({
586
 
                'bond_interfaces': copy.deepcopy(link['bond_links']),
587
 
                'params': params,
588
 
            })
589
 
        elif link['type'] in ['vlan']:
590
 
            cfg.update({
591
 
                'name': "%s.%s" % (link['vlan_link'],
592
 
                                   link['vlan_id']),
593
 
                'vlan_link': link['vlan_link'],
594
 
                'vlan_id': link['vlan_id'],
595
 
                'mac_address': link['vlan_mac_address'],
596
 
            })
597
 
        else:
598
 
            raise ValueError(
599
 
                'Unknown network_data link type: %s' % link['type'])
600
 
 
601
 
        config.append(cfg)
602
 
 
603
 
    need_names = [d for d in config
604
 
                  if d.get('type') == 'physical' and 'name' not in d]
605
 
 
606
 
    if need_names:
607
 
        if known_macs is None:
608
 
            known_macs = net.get_interfaces_by_mac()
609
 
 
610
 
        for d in need_names:
611
 
            mac = d.get('mac_address')
612
 
            if not mac:
613
 
                raise ValueError("No mac_address or name entry for %s" % d)
614
 
            if mac not in known_macs:
615
 
                raise ValueError("Unable to find a system nic for %s" % d)
616
 
            d['name'] = known_macs[mac]
617
 
 
618
 
    for service in services:
619
 
        cfg = service
620
 
        cfg.update({'type': 'nameserver'})
621
 
        config.append(cfg)
622
 
 
623
 
    return {'version': 1, 'config': config}
624
 
 
625
 
 
626
 
def convert_vendordata_json(data, recurse=True):
627
 
    """data: a loaded json *object* (strings, arrays, dicts).
628
 
    return something suitable for cloudinit vendordata_raw.
629
 
 
630
 
    if data is:
631
 
       None: return None
632
 
       string: return string
633
 
       list: return data
634
 
             the list is then processed in UserDataProcessor
635
 
       dict: return convert_vendordata_json(data.get('cloud-init'))
636
 
    """
637
 
    if not data:
638
 
        return None
639
 
    if isinstance(data, six.string_types):
640
 
        return data
641
 
    if isinstance(data, list):
642
 
        return copy.deepcopy(data)
643
 
    if isinstance(data, dict):
644
 
        if recurse is True:
645
 
            return convert_vendordata_json(data.get('cloud-init'),
646
 
                                           recurse=False)
647
 
        raise ValueError("vendordata['cloud-init'] cannot be dict")
648
 
    raise ValueError("Unknown data type for vendordata: %s" % type(data))