Blame cloudinit/sources/DataSourceNoCloud.py

Packit Service a04d08
# Copyright (C) 2009-2010 Canonical Ltd.
Packit Service a04d08
# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
Packit Service a04d08
# Copyright (C) 2012 Yahoo! Inc.
Packit Service a04d08
#
Packit Service a04d08
# Author: Scott Moser <scott.moser@canonical.com>
Packit Service a04d08
# Author: Juerg Hafliger <juerg.haefliger@hp.com>
Packit Service a04d08
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
Packit Service a04d08
#
Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
import errno
Packit Service a04d08
import os
Packit Service a04d08
Packit Service a04d08
from cloudinit import log as logging
Packit Service a04d08
from cloudinit.net import eni
Packit Service a04d08
from cloudinit import sources
Packit Service a04d08
from cloudinit import util
Packit Service a04d08
Packit Service a04d08
LOG = logging.getLogger(__name__)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class DataSourceNoCloud(sources.DataSource):
Packit Service a04d08
Packit Service a04d08
    dsname = "NoCloud"
Packit Service a04d08
Packit Service a04d08
    def __init__(self, sys_cfg, distro, paths):
Packit Service a04d08
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
Packit Service a04d08
        self.seed = None
Packit Service a04d08
        self.seed_dirs = [os.path.join(paths.seed_dir, 'nocloud'),
Packit Service a04d08
                          os.path.join(paths.seed_dir, 'nocloud-net')]
Packit Service a04d08
        self.seed_dir = None
Packit Service a04d08
        self.supported_seed_starts = ("/", "file://")
Packit Service a04d08
Packit Service a04d08
    def __str__(self):
Packit Service a04d08
        root = sources.DataSource.__str__(self)
Packit Service a04d08
        return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
Packit Service a04d08
Packit Service a04d08
    def _get_devices(self, label):
Packit Service 751c4a
        fslist = util.find_devs_with("TYPE=vfat")
Packit Service 751c4a
        fslist.extend(util.find_devs_with("TYPE=iso9660"))
Packit Service a04d08
Packit Service 751c4a
        label_list = util.find_devs_with("LABEL=%s" % label.upper())
Packit Service 751c4a
        label_list.extend(util.find_devs_with("LABEL=%s" % label.lower()))
Packit Service 751c4a
        label_list.extend(util.find_devs_with("LABEL_FATBOOT=%s" % label))
Packit Service a04d08
Packit Service 751c4a
        devlist = list(set(fslist) & set(label_list))
Packit Service 751c4a
        devlist.sort(reverse=True)
Packit Service a04d08
        return devlist
Packit Service a04d08
Packit Service a04d08
    def _get_data(self):
Packit Service a04d08
        defaults = {
Packit Service a04d08
            "instance-id": "nocloud",
Packit Service a04d08
            "dsmode": self.dsmode,
Packit Service a04d08
        }
Packit Service a04d08
Packit Service a04d08
        found = []
Packit Service a04d08
        mydata = {'meta-data': {}, 'user-data': "", 'vendor-data': "",
Packit Service a04d08
                  'network-config': None}
Packit Service a04d08
Packit Service a04d08
        try:
Packit Service a04d08
            # Parse the system serial label from dmi. If not empty, try parsing
Packit Service a04d08
            # like the commandline
Packit Service a04d08
            md = {}
Packit Service a04d08
            serial = util.read_dmi_data('system-serial-number')
Packit Service a04d08
            if serial and load_cmdline_data(md, serial):
Packit Service a04d08
                found.append("dmi")
Packit Service a04d08
                mydata = _merge_new_seed(mydata, {'meta-data': md})
Packit Service a04d08
        except Exception:
Packit Service a04d08
            util.logexc(LOG, "Unable to parse dmi data")
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        try:
Packit Service a04d08
            # Parse the kernel command line, getting data passed in
Packit Service a04d08
            md = {}
Packit Service a04d08
            if load_cmdline_data(md):
Packit Service a04d08
                found.append("cmdline")
Packit Service a04d08
                mydata = _merge_new_seed(mydata, {'meta-data': md})
Packit Service a04d08
        except Exception:
Packit Service a04d08
            util.logexc(LOG, "Unable to parse command line data")
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        # Check to see if the seed dir has data.
Packit Service a04d08
        pp2d_kwargs = {'required': ['user-data', 'meta-data'],
Packit Service a04d08
                       'optional': ['vendor-data', 'network-config']}
Packit Service a04d08
Packit Service a04d08
        for path in self.seed_dirs:
Packit Service a04d08
            try:
Packit Service a04d08
                seeded = util.pathprefix2dict(path, **pp2d_kwargs)
Packit Service a04d08
                found.append(path)
Packit Service a04d08
                LOG.debug("Using seeded data from %s", path)
Packit Service a04d08
                mydata = _merge_new_seed(mydata, seeded)
Packit Service a04d08
                break
Packit Service a04d08
            except ValueError:
Packit Service a04d08
                pass
Packit Service a04d08
Packit Service a04d08
        # If the datasource config had a 'seedfrom' entry, then that takes
Packit Service a04d08
        # precedence over a 'seedfrom' that was found in a filesystem
Packit Service a04d08
        # but not over external media
Packit Service a04d08
        if self.ds_cfg.get('seedfrom'):
Packit Service a04d08
            found.append("ds_config_seedfrom")
Packit Service a04d08
            mydata['meta-data']["seedfrom"] = self.ds_cfg['seedfrom']
Packit Service a04d08
Packit Service a04d08
        # fields appropriately named can also just come from the datasource
Packit Service a04d08
        # config (ie, 'user-data', 'meta-data', 'vendor-data' there)
Packit Service a04d08
        if 'user-data' in self.ds_cfg and 'meta-data' in self.ds_cfg:
Packit Service a04d08
            mydata = _merge_new_seed(mydata, self.ds_cfg)
Packit Service a04d08
            found.append("ds_config")
Packit Service a04d08
Packit Service a04d08
        def _pp2d_callback(mp, data):
Packit Service a04d08
            return util.pathprefix2dict(mp, **data)
Packit Service a04d08
Packit Service a04d08
        label = self.ds_cfg.get('fs_label', "cidata")
Packit Service a04d08
        if label is not None:
Packit Service a04d08
            for dev in self._get_devices(label):
Packit Service a04d08
                try:
Packit Service a04d08
                    LOG.debug("Attempting to use data from %s", dev)
Packit Service a04d08
Packit Service a04d08
                    try:
Packit Service a04d08
                        seeded = util.mount_cb(dev, _pp2d_callback,
Packit Service a04d08
                                               pp2d_kwargs)
Packit Service a04d08
                    except ValueError:
Packit Service a04d08
                        LOG.warning("device %s with label=%s not a "
Packit Service a04d08
                                    "valid seed.", dev, label)
Packit Service a04d08
                        continue
Packit Service a04d08
Packit Service a04d08
                    mydata = _merge_new_seed(mydata, seeded)
Packit Service a04d08
Packit Service a04d08
                    LOG.debug("Using data from %s", dev)
Packit Service a04d08
                    found.append(dev)
Packit Service a04d08
                    break
Packit Service a04d08
                except OSError as e:
Packit Service a04d08
                    if e.errno != errno.ENOENT:
Packit Service a04d08
                        raise
Packit Service a04d08
                except util.MountFailedError:
Packit Service a04d08
                    util.logexc(LOG, "Failed to mount %s when looking for "
Packit Service a04d08
                                "data", dev)
Packit Service a04d08
Packit Service a04d08
        # There was no indication on kernel cmdline or data
Packit Service a04d08
        # in the seeddir suggesting this handler should be used.
Packit Service a04d08
        if len(found) == 0:
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        # The special argument "seedfrom" indicates we should
Packit Service a04d08
        # attempt to seed the userdata / metadata from its value
Packit Service a04d08
        # its primarily value is in allowing the user to type less
Packit Service a04d08
        # on the command line, ie: ds=nocloud;s=http://bit.ly/abcdefg
Packit Service a04d08
        if "seedfrom" in mydata['meta-data']:
Packit Service a04d08
            seedfrom = mydata['meta-data']["seedfrom"]
Packit Service a04d08
            seedfound = False
Packit Service a04d08
            for proto in self.supported_seed_starts:
Packit Service a04d08
                if seedfrom.startswith(proto):
Packit Service a04d08
                    seedfound = proto
Packit Service a04d08
                    break
Packit Service a04d08
            if not seedfound:
Packit Service a04d08
                LOG.debug("Seed from %s not supported by %s", seedfrom, self)
Packit Service a04d08
                return False
Packit Service a04d08
Packit Service a04d08
            # This could throw errors, but the user told us to do it
Packit Service a04d08
            # so if errors are raised, let them raise
Packit Service a04d08
            (md_seed, ud) = util.read_seeded(seedfrom, timeout=None)
Packit Service a04d08
            LOG.debug("Using seeded cache data from %s", seedfrom)
Packit Service a04d08
Packit Service a04d08
            # Values in the command line override those from the seed
Packit Service a04d08
            mydata['meta-data'] = util.mergemanydict([mydata['meta-data'],
Packit Service a04d08
                                                      md_seed])
Packit Service a04d08
            mydata['user-data'] = ud
Packit Service a04d08
            found.append(seedfrom)
Packit Service a04d08
Packit Service a04d08
        # Now that we have exhausted any other places merge in the defaults
Packit Service a04d08
        mydata['meta-data'] = util.mergemanydict([mydata['meta-data'],
Packit Service a04d08
                                                  defaults])
Packit Service a04d08
Packit Service a04d08
        self.dsmode = self._determine_dsmode(
Packit Service a04d08
            [mydata['meta-data'].get('dsmode')])
Packit Service a04d08
Packit Service a04d08
        if self.dsmode == sources.DSMODE_DISABLED:
Packit Service a04d08
            LOG.debug("%s: not claiming datasource, dsmode=%s", self,
Packit Service a04d08
                      self.dsmode)
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        self.seed = ",".join(found)
Packit Service a04d08
        self.metadata = mydata['meta-data']
Packit Service a04d08
        self.userdata_raw = mydata['user-data']
Packit Service a04d08
        self.vendordata_raw = mydata['vendor-data']
Packit Service a04d08
        self._network_config = mydata['network-config']
Packit Service a04d08
        self._network_eni = mydata['meta-data'].get('network-interfaces')
Packit Service a04d08
        return True
Packit Service a04d08
Packit Service a04d08
    @property
Packit Service a04d08
    def platform_type(self):
Packit Service a04d08
        # Handle upgrade path of pickled ds
Packit Service a04d08
        if not hasattr(self, '_platform_type'):
Packit Service a04d08
            self._platform_type = None
Packit Service a04d08
        if not self._platform_type:
Packit Service a04d08
            self._platform_type = 'lxd' if util.is_lxd() else 'nocloud'
Packit Service a04d08
        return self._platform_type
Packit Service a04d08
Packit Service a04d08
    def _get_cloud_name(self):
Packit Service a04d08
        """Return unknown when 'cloud-name' key is absent from metadata."""
Packit Service a04d08
        return sources.METADATA_UNKNOWN
Packit Service a04d08
Packit Service a04d08
    def _get_subplatform(self):
Packit Service a04d08
        """Return the subplatform metadata source details."""
Packit Service a04d08
        if self.seed.startswith('/dev'):
Packit Service a04d08
            subplatform_type = 'config-disk'
Packit Service a04d08
        else:
Packit Service a04d08
            subplatform_type = 'seed-dir'
Packit Service a04d08
        return '%s (%s)' % (subplatform_type, self.seed)
Packit Service a04d08
Packit Service a04d08
    def check_instance_id(self, sys_cfg):
Packit Service a04d08
        # quickly (local check only) if self.instance_id is still valid
Packit Service a04d08
        # we check kernel command line or files.
Packit Service a04d08
        current = self.get_instance_id()
Packit Service a04d08
        if not current:
Packit Service a04d08
            return None
Packit Service a04d08
Packit Service a04d08
        # LP: #1568150 need getattr in the case that an old class object
Packit Service a04d08
        # has been loaded from a pickled file and now executing new source.
Packit Service a04d08
        dirs = getattr(self, 'seed_dirs', [self.seed_dir])
Packit Service a04d08
        quick_id = _quick_read_instance_id(dirs=dirs)
Packit Service a04d08
        if not quick_id:
Packit Service a04d08
            return None
Packit Service a04d08
        return quick_id == current
Packit Service a04d08
Packit Service a04d08
    @property
Packit Service a04d08
    def network_config(self):
Packit Service a04d08
        if self._network_config is None:
Packit Service a04d08
            if self._network_eni is not None:
Packit Service a04d08
                self._network_config = eni.convert_eni_data(self._network_eni)
Packit Service a04d08
        return self._network_config
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _quick_read_instance_id(dirs=None):
Packit Service a04d08
    if dirs is None:
Packit Service a04d08
        dirs = []
Packit Service a04d08
Packit Service a04d08
    iid_key = 'instance-id'
Packit Service a04d08
    fill = {}
Packit Service a04d08
    if load_cmdline_data(fill) and iid_key in fill:
Packit Service a04d08
        return fill[iid_key]
Packit Service a04d08
Packit Service a04d08
    for d in dirs:
Packit Service a04d08
        if d is None:
Packit Service a04d08
            continue
Packit Service a04d08
        try:
Packit Service a04d08
            data = util.pathprefix2dict(d, required=['meta-data'])
Packit Service a04d08
            md = util.load_yaml(data['meta-data'])
Packit Service a04d08
            if iid_key in md:
Packit Service a04d08
                return md[iid_key]
Packit Service a04d08
        except ValueError:
Packit Service a04d08
            pass
Packit Service a04d08
Packit Service a04d08
    return None
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def load_cmdline_data(fill, cmdline=None):
Packit Service a04d08
    pairs = [("ds=nocloud", sources.DSMODE_LOCAL),
Packit Service a04d08
             ("ds=nocloud-net", sources.DSMODE_NETWORK)]
Packit Service a04d08
    for idstr, dsmode in pairs:
Packit Service a04d08
        if parse_cmdline_data(idstr, fill, cmdline):
Packit Service a04d08
            # if dsmode was explicitly in the command line, then
Packit Service a04d08
            # prefer it to the dsmode based on the command line id
Packit Service a04d08
            if 'dsmode' not in fill:
Packit Service a04d08
                fill['dsmode'] = dsmode
Packit Service a04d08
            return True
Packit Service a04d08
    return False
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# Returns true or false indicating if cmdline indicated
Packit Service a04d08
# that this module should be used.  Updates dictionary 'fill'
Packit Service a04d08
# with data that was found.
Packit Service a04d08
# Example cmdline:
Packit Service a04d08
#  root=LABEL=uec-rootfs ro ds=nocloud
Packit Service a04d08
def parse_cmdline_data(ds_id, fill, cmdline=None):
Packit Service a04d08
    if cmdline is None:
Packit Service a04d08
        cmdline = util.get_cmdline()
Packit Service a04d08
    cmdline = " %s " % cmdline
Packit Service a04d08
Packit Service a04d08
    if not (" %s " % ds_id in cmdline or " %s;" % ds_id in cmdline):
Packit Service a04d08
        return False
Packit Service a04d08
Packit Service a04d08
    argline = ""
Packit Service a04d08
    # cmdline can contain:
Packit Service a04d08
    # ds=nocloud[;key=val;key=val]
Packit Service a04d08
    for tok in cmdline.split():
Packit Service a04d08
        if tok.startswith(ds_id):
Packit Service a04d08
            argline = tok.split("=", 1)
Packit Service a04d08
Packit Service a04d08
    # argline array is now 'nocloud' followed optionally by
Packit Service a04d08
    # a ';' and then key=value pairs also terminated with ';'
Packit Service a04d08
    tmp = argline[1].split(";")
Packit Service a04d08
    if len(tmp) > 1:
Packit Service a04d08
        kvpairs = tmp[1:]
Packit Service a04d08
    else:
Packit Service a04d08
        kvpairs = ()
Packit Service a04d08
Packit Service a04d08
    # short2long mapping to save cmdline typing
Packit Service a04d08
    s2l = {"h": "local-hostname", "i": "instance-id", "s": "seedfrom"}
Packit Service a04d08
    for item in kvpairs:
Packit Service a04d08
        if item == "":
Packit Service a04d08
            continue
Packit Service a04d08
        try:
Packit Service a04d08
            (k, v) = item.split("=", 1)
Packit Service a04d08
        except Exception:
Packit Service a04d08
            k = item
Packit Service a04d08
            v = None
Packit Service a04d08
        if k in s2l:
Packit Service a04d08
            k = s2l[k]
Packit Service a04d08
        fill[k] = v
Packit Service a04d08
Packit Service a04d08
    return True
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _maybe_remove_top_network(cfg):
Packit Service a04d08
    """If network-config contains top level 'network' key, then remove it.
Packit Service a04d08
Packit Service a04d08
    Some providers of network configuration may provide a top level
Packit Service a04d08
    'network' key (LP: #1798117) even though it is not necessary.
Packit Service a04d08
Packit Service a04d08
    Be friendly and remove it if it really seems so.
Packit Service a04d08
Packit Service a04d08
    Return the original value if no change or the updated value if changed."""
Packit Service a04d08
    nullval = object()
Packit Service a04d08
    network_val = cfg.get('network', nullval)
Packit Service a04d08
    if network_val is nullval:
Packit Service a04d08
        return cfg
Packit Service a04d08
    bmsg = 'Top level network key in network-config %s: %s'
Packit Service a04d08
    if not isinstance(network_val, dict):
Packit Service a04d08
        LOG.debug(bmsg, "was not a dict", cfg)
Packit Service a04d08
        return cfg
Packit Service a04d08
    if len(list(cfg.keys())) != 1:
Packit Service a04d08
        LOG.debug(bmsg, "had multiple top level keys", cfg)
Packit Service a04d08
        return cfg
Packit Service a04d08
    if network_val.get('config') == "disabled":
Packit Service a04d08
        LOG.debug(bmsg, "was config/disabled", cfg)
Packit Service a04d08
    elif not all(('config' in network_val, 'version' in network_val)):
Packit Service a04d08
        LOG.debug(bmsg, "but missing 'config' or 'version'", cfg)
Packit Service a04d08
        return cfg
Packit Service a04d08
    LOG.debug(bmsg, "fixed by removing shifting network.", cfg)
Packit Service a04d08
    return network_val
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _merge_new_seed(cur, seeded):
Packit Service a04d08
    ret = cur.copy()
Packit Service a04d08
Packit Service a04d08
    newmd = seeded.get('meta-data', {})
Packit Service a04d08
    if not isinstance(seeded['meta-data'], dict):
Packit Service a04d08
        newmd = util.load_yaml(seeded['meta-data'])
Packit Service a04d08
    ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd])
Packit Service a04d08
Packit Service a04d08
    if seeded.get('network-config'):
Packit Service a04d08
        ret['network-config'] = _maybe_remove_top_network(
Packit Service a04d08
            util.load_yaml(seeded.get('network-config')))
Packit Service a04d08
Packit Service a04d08
    if 'user-data' in seeded:
Packit Service a04d08
        ret['user-data'] = seeded['user-data']
Packit Service a04d08
    if 'vendor-data' in seeded:
Packit Service a04d08
        ret['vendor-data'] = seeded['vendor-data']
Packit Service a04d08
    return ret
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class DataSourceNoCloudNet(DataSourceNoCloud):
Packit Service a04d08
    def __init__(self, sys_cfg, distro, paths):
Packit Service a04d08
        DataSourceNoCloud.__init__(self, sys_cfg, distro, paths)
Packit Service 751c4a
        self.supported_seed_starts = ("http://", "https://")
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# Used to match classes to dependencies
Packit Service a04d08
datasources = [
Packit Service a04d08
    (DataSourceNoCloud, (sources.DEP_FILESYSTEM, )),
Packit Service a04d08
    (DataSourceNoCloudNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
Packit Service a04d08
]
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# Return a list of data sources that match this set of dependencies
Packit Service a04d08
def get_datasource_list(depends):
Packit Service a04d08
    return sources.list_from_depends(depends, datasources)
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab