Blame cloudinit/sources/DataSourceMAAS.py

Packit Service a04d08
# Copyright (C) 2012 Canonical Ltd.
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: 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 hashlib
Packit Service a04d08
import os
Packit Service a04d08
import time
Packit Service a04d08
Packit Service a04d08
from cloudinit import log as logging
Packit Service a04d08
from cloudinit import sources
Packit Service a04d08
from cloudinit import url_helper
Packit Service a04d08
from cloudinit import util
Packit Service a04d08
Packit Service a04d08
LOG = logging.getLogger(__name__)
Packit Service a04d08
MD_VERSION = "2012-03-01"
Packit Service a04d08
Packit Service a04d08
DS_FIELDS = [
Packit Service a04d08
    # remote path, location in dictionary, binary data?, optional?
Packit Service a04d08
    ("meta-data/instance-id", 'meta-data/instance-id', False, False),
Packit Service a04d08
    ("meta-data/local-hostname", 'meta-data/local-hostname', False, False),
Packit Service a04d08
    ("meta-data/public-keys", 'meta-data/public-keys', False, True),
Packit Service a04d08
    ('meta-data/vendor-data', 'vendor-data', True, True),
Packit Service a04d08
    ('user-data', 'user-data', True, True),
Packit Service a04d08
]
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class DataSourceMAAS(sources.DataSource):
Packit Service a04d08
    """
Packit Service a04d08
    DataSourceMAAS reads instance information from MAAS.
Packit Service a04d08
    Given a config metadata_url, and oauth tokens, it expects to find
Packit Service a04d08
    files under the root named:
Packit Service a04d08
      instance-id
Packit Service a04d08
      user-data
Packit Service a04d08
      hostname
Packit Service a04d08
      vendor-data
Packit Service a04d08
    """
Packit Service a04d08
Packit Service a04d08
    dsname = "MAAS"
Packit Service a04d08
    id_hash = None
Packit Service a04d08
    _oauth_helper = None
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.base_url = None
Packit Service a04d08
        self.seed_dir = os.path.join(paths.seed_dir, 'maas')
Packit Service a04d08
        self.id_hash = get_id_from_ds_cfg(self.ds_cfg)
Packit Service a04d08
Packit Service a04d08
    @property
Packit Service a04d08
    def oauth_helper(self):
Packit Service a04d08
        if not self._oauth_helper:
Packit Service a04d08
            self._oauth_helper = get_oauth_helper(self.ds_cfg)
Packit Service a04d08
        return self._oauth_helper
Packit Service a04d08
Packit Service a04d08
    def __str__(self):
Packit Service a04d08
        root = sources.DataSource.__str__(self)
Packit Service a04d08
        return "%s [%s]" % (root, self.base_url)
Packit Service a04d08
Packit Service a04d08
    def _get_data(self):
Packit Service a04d08
        mcfg = self.ds_cfg
Packit Service a04d08
Packit Service a04d08
        try:
Packit Service a04d08
            self._set_data(self.seed_dir, read_maas_seed_dir(self.seed_dir))
Packit Service a04d08
            return True
Packit Service a04d08
        except MAASSeedDirNone:
Packit Service a04d08
            pass
Packit Service a04d08
        except MAASSeedDirMalformed as exc:
Packit Service a04d08
            LOG.warning("%s was malformed: %s", self.seed_dir, exc)
Packit Service a04d08
            raise
Packit Service a04d08
Packit Service a04d08
        # If there is no metadata_url, then we're not configured
Packit Service a04d08
        url = mcfg.get('metadata_url', None)
Packit Service a04d08
        if not url:
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        try:
Packit Service a04d08
            # doing this here actually has a side affect of
Packit Service a04d08
            # getting oauth time-fix in place.  As no where else would
Packit Service a04d08
            # retry by default, so even if we could fix the timestamp
Packit Service a04d08
            # we would not.
Packit Service a04d08
            if not self.wait_for_metadata_service(url):
Packit Service a04d08
                return False
Packit Service a04d08
Packit Service a04d08
            self._set_data(
Packit Service a04d08
                url, read_maas_seed_url(
Packit Service a04d08
                    url, read_file_or_url=self.oauth_helper.readurl,
Packit Service a04d08
                    paths=self.paths, retries=1))
Packit Service a04d08
            return True
Packit Service a04d08
        except Exception:
Packit Service a04d08
            util.logexc(LOG, "Failed fetching metadata from url %s", url)
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
    def _set_data(self, url, data):
Packit Service a04d08
        # takes a url for base_url and a tuple of userdata, metadata, vd.
Packit Service a04d08
        self.base_url = url
Packit Service a04d08
        ud, md, vd = data
Packit Service a04d08
        self.userdata_raw = ud
Packit Service a04d08
        self.metadata = md
Packit Service a04d08
        self.vendordata_pure = vd
Packit Service a04d08
        if vd:
Packit Service a04d08
            try:
Packit Service a04d08
                self.vendordata_raw = sources.convert_vendordata(vd)
Packit Service a04d08
            except ValueError as e:
Packit Service a04d08
                LOG.warning("Invalid content in vendor-data: %s", e)
Packit Service a04d08
                self.vendordata_raw = None
Packit Service a04d08
Packit Service a04d08
    def _get_subplatform(self):
Packit Service a04d08
        """Return the subplatform metadata source details."""
Packit Service a04d08
        return 'seed-dir (%s)' % self.base_url
Packit Service a04d08
Packit Service a04d08
    def wait_for_metadata_service(self, url):
Packit Service a04d08
        mcfg = self.ds_cfg
Packit Service a04d08
        max_wait = 120
Packit Service a04d08
        try:
Packit Service a04d08
            max_wait = int(mcfg.get("max_wait", max_wait))
Packit Service a04d08
        except Exception:
Packit Service a04d08
            util.logexc(LOG, "Failed to get max wait. using %s", max_wait)
Packit Service a04d08
Packit Service a04d08
        if max_wait == 0:
Packit Service a04d08
            return False
Packit Service a04d08
Packit Service a04d08
        timeout = 50
Packit Service a04d08
        try:
Packit Service a04d08
            if timeout in mcfg:
Packit Service a04d08
                timeout = int(mcfg.get("timeout", timeout))
Packit Service a04d08
        except Exception:
Packit Service a04d08
            LOG.warning("Failed to get timeout, using %s", timeout)
Packit Service a04d08
Packit Service a04d08
        starttime = time.time()
Packit Service a04d08
        if url.endswith("/"):
Packit Service a04d08
            url = url[:-1]
Packit Service a04d08
        check_url = "%s/%s/meta-data/instance-id" % (url, MD_VERSION)
Packit Service a04d08
        urls = [check_url]
Packit Service a04d08
        url, _response = self.oauth_helper.wait_for_url(
Packit Service a04d08
            urls=urls, max_wait=max_wait, timeout=timeout)
Packit Service a04d08
Packit Service a04d08
        if url:
Packit Service a04d08
            LOG.debug("Using metadata source: '%s'", url)
Packit Service a04d08
        else:
Packit Service a04d08
            LOG.critical("Giving up on md from %s after %i seconds",
Packit Service a04d08
                         urls, int(time.time() - starttime))
Packit Service a04d08
Packit Service a04d08
        return bool(url)
Packit Service a04d08
Packit Service a04d08
    def check_instance_id(self, sys_cfg):
Packit Service a04d08
        """locally check if the current system is the same instance.
Packit Service a04d08
Packit Service a04d08
        MAAS doesn't provide a real instance-id, and if it did, it is
Packit Service a04d08
        still only available over the network.  We need to check based
Packit Service a04d08
        only on local resources.  So compute a hash based on Oauth tokens."""
Packit Service a04d08
        if self.id_hash is None:
Packit Service a04d08
            return False
Packit Service a04d08
        ncfg = util.get_cfg_by_path(sys_cfg, ("datasource", self.dsname), {})
Packit Service a04d08
        return (self.id_hash == get_id_from_ds_cfg(ncfg))
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def get_oauth_helper(cfg):
Packit Service a04d08
    """Return an oauth helper instance for values in cfg.
Packit Service a04d08
Packit Service a04d08
       @raises ValueError from OauthUrlHelper if some required fields have
Packit Service a04d08
               true-ish values but others do not."""
Packit Service a04d08
    keys = ('consumer_key', 'consumer_secret', 'token_key', 'token_secret')
Packit Service a04d08
    kwargs = dict([(r, cfg.get(r)) for r in keys])
Packit Service a04d08
    return url_helper.OauthUrlHelper(**kwargs)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def get_id_from_ds_cfg(ds_cfg):
Packit Service a04d08
    """Given a config, generate a unique identifier for this node."""
Packit Service a04d08
    fields = ('consumer_key', 'token_key', 'token_secret')
Packit Service a04d08
    idstr = '\0'.join([ds_cfg.get(f, "") for f in fields])
Packit Service a04d08
    # store the encoding version as part of the hash in the event
Packit Service a04d08
    # that it ever changed we can compute older versions.
Packit Service a04d08
    return 'v1:' + hashlib.sha256(idstr.encode('utf-8')).hexdigest()
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def read_maas_seed_dir(seed_d):
Packit Service a04d08
    if seed_d.startswith("file://"):
Packit Service a04d08
        seed_d = seed_d[7:]
Packit Service a04d08
    if not os.path.isdir(seed_d) or len(os.listdir(seed_d)) == 0:
Packit Service a04d08
        raise MAASSeedDirNone("%s: not a directory")
Packit Service a04d08
Packit Service a04d08
    # seed_dir looks in seed_dir, not seed_dir/VERSION
Packit Service a04d08
    return read_maas_seed_url("file://%s" % seed_d, version=None)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def read_maas_seed_url(seed_url, read_file_or_url=None, timeout=None,
Packit Service a04d08
                       version=MD_VERSION, paths=None, retries=None):
Packit Service a04d08
    """
Packit Service a04d08
    Read the maas datasource at seed_url.
Packit Service a04d08
      read_file_or_url is a method that should provide an interface
Packit Service a04d08
      like util.read_file_or_url
Packit Service a04d08
Packit Service a04d08
    Expected format of seed_url is are the following files:
Packit Service a04d08
      * <seed_url>/<version>/meta-data/instance-id
Packit Service a04d08
      * <seed_url>/<version>/meta-data/local-hostname
Packit Service a04d08
      * <seed_url>/<version>/user-data
Packit Service a04d08
    If version is None, then <version>/ will not be used.
Packit Service a04d08
    """
Packit Service a04d08
    if read_file_or_url is None:
Packit Service a04d08
        read_file_or_url = url_helper.read_file_or_url
Packit Service a04d08
Packit Service a04d08
    if seed_url.endswith("/"):
Packit Service a04d08
        seed_url = seed_url[:-1]
Packit Service a04d08
Packit Service a04d08
    md = {}
Packit Service a04d08
    for path, _dictname, binary, optional in DS_FIELDS:
Packit Service a04d08
        if version is None:
Packit Service a04d08
            url = "%s/%s" % (seed_url, path)
Packit Service a04d08
        else:
Packit Service a04d08
            url = "%s/%s/%s" % (seed_url, version, path)
Packit Service a04d08
        try:
Packit Service a04d08
            ssl_details = util.fetch_ssl_details(paths)
Packit Service a04d08
            resp = read_file_or_url(url, retries=retries, timeout=timeout,
Packit Service a04d08
                                    ssl_details=ssl_details)
Packit Service a04d08
            if resp.ok():
Packit Service a04d08
                if binary:
Packit Service a04d08
                    md[path] = resp.contents
Packit Service a04d08
                else:
Packit Service a04d08
                    md[path] = util.decode_binary(resp.contents)
Packit Service a04d08
            else:
Packit Service a04d08
                LOG.warning(("Fetching from %s resulted in"
Packit Service a04d08
                             " an invalid http code %s"), url, resp.code)
Packit Service a04d08
        except url_helper.UrlError as e:
Packit Service a04d08
            if e.code == 404 and not optional:
Packit Service a04d08
                raise MAASSeedDirMalformed(
Packit Service 9bfd13
                    "Missing required %s: %s" % (path, e)
Packit Service 9bfd13
                ) from e
Packit Service a04d08
            elif e.code != 404:
Packit Service a04d08
                raise e
Packit Service a04d08
Packit Service a04d08
    return check_seed_contents(md, seed_url)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def check_seed_contents(content, seed):
Packit Service a04d08
    """Validate if dictionary content valid as a return for a datasource.
Packit Service a04d08
       Either return a (userdata, metadata, vendordata) tuple or
Packit Service a04d08
       Raise MAASSeedDirMalformed or MAASSeedDirNone
Packit Service a04d08
    """
Packit Service a04d08
    ret = {}
Packit Service a04d08
    missing = []
Packit Service a04d08
    for spath, dpath, _binary, optional in DS_FIELDS:
Packit Service a04d08
        if spath not in content:
Packit Service a04d08
            if not optional:
Packit Service a04d08
                missing.append(spath)
Packit Service a04d08
            continue
Packit Service a04d08
Packit Service a04d08
        if "/" in dpath:
Packit Service a04d08
            top, _, p = dpath.partition("/")
Packit Service a04d08
            if top not in ret:
Packit Service a04d08
                ret[top] = {}
Packit Service a04d08
            ret[top][p] = content[spath]
Packit Service a04d08
        else:
Packit Service a04d08
            ret[dpath] = content[spath]
Packit Service a04d08
Packit Service a04d08
    if len(ret) == 0:
Packit Service a04d08
        raise MAASSeedDirNone("%s: no data files found" % seed)
Packit Service a04d08
Packit Service a04d08
    if missing:
Packit Service a04d08
        raise MAASSeedDirMalformed("%s: missing files %s" % (seed, missing))
Packit Service a04d08
Packit Service a04d08
    vd_data = None
Packit Service a04d08
    if ret.get('vendor-data'):
Packit Service a04d08
        err = object()
Packit Service a04d08
        vd_data = util.load_yaml(ret.get('vendor-data'), default=err,
Packit Service a04d08
                                 allowed=(object))
Packit Service a04d08
        if vd_data is err:
Packit Service a04d08
            raise MAASSeedDirMalformed("vendor-data was not loadable as yaml.")
Packit Service a04d08
Packit Service a04d08
    return ret.get('user-data'), ret.get('meta-data'), vd_data
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class MAASSeedDirNone(Exception):
Packit Service a04d08
    pass
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class MAASSeedDirMalformed(Exception):
Packit Service a04d08
    pass
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# Used to match classes to dependencies
Packit Service a04d08
datasources = [
Packit Service a04d08
    (DataSourceMAAS, (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
Packit Service a04d08
if __name__ == "__main__":
Packit Service a04d08
    def main():
Packit Service a04d08
        """
Packit Service a04d08
        Call with single argument of directory or http or https url.
Packit Service a04d08
        If url is given additional arguments are allowed, which will be
Packit Service a04d08
        interpreted as consumer_key, token_key, token_secret, consumer_secret
Packit Service a04d08
        """
Packit Service a04d08
        import argparse
Packit Service a04d08
        import pprint
Packit Service a04d08
        import sys
Packit Service a04d08
Packit Service a04d08
        parser = argparse.ArgumentParser(description='Interact with MAAS DS')
Packit Service a04d08
        parser.add_argument("--config", metavar="file",
Packit Service a04d08
                            help="specify DS config file", default=None)
Packit Service a04d08
        parser.add_argument("--ckey", metavar="key",
Packit Service a04d08
                            help="the consumer key to auth with", default=None)
Packit Service a04d08
        parser.add_argument("--tkey", metavar="key",
Packit Service a04d08
                            help="the token key to auth with", default=None)
Packit Service a04d08
        parser.add_argument("--csec", metavar="secret",
Packit Service a04d08
                            help="the consumer secret (likely '')", default="")
Packit Service a04d08
        parser.add_argument("--tsec", metavar="secret",
Packit Service a04d08
                            help="the token secret to auth with", default=None)
Packit Service a04d08
        parser.add_argument("--apiver", metavar="version",
Packit Service a04d08
                            help="the apiver to use ("" can be used)",
Packit Service a04d08
                            default=MD_VERSION)
Packit Service a04d08
Packit Service a04d08
        subcmds = parser.add_subparsers(title="subcommands", dest="subcmd")
Packit Service a04d08
        for (name, help) in (('crawl', 'crawl the datasource'),
Packit Service a04d08
                             ('get', 'do a single GET of provided url'),
Packit Service a04d08
                             ('check-seed', 'read and verify seed at url')):
Packit Service a04d08
            p = subcmds.add_parser(name, help=help)
Packit Service a04d08
            p.add_argument("url", help="the datasource url", nargs='?',
Packit Service a04d08
                           default=None)
Packit Service a04d08
Packit Service a04d08
        args = parser.parse_args()
Packit Service a04d08
Packit Service a04d08
        creds = {'consumer_key': args.ckey, 'token_key': args.tkey,
Packit Service a04d08
                 'token_secret': args.tsec, 'consumer_secret': args.csec}
Packit Service a04d08
Packit Service a04d08
        if args.config is None:
Packit Service a04d08
            for fname in ('91_kernel_cmdline_url', '90_dpkg_maas'):
Packit Service a04d08
                fpath = "/etc/cloud/cloud.cfg.d/" + fname + ".cfg"
Packit Service a04d08
                if os.path.exists(fpath) and os.access(fpath, os.R_OK):
Packit Service a04d08
                    sys.stderr.write("Used config in %s.\n" % fpath)
Packit Service a04d08
                    args.config = fpath
Packit Service a04d08
Packit Service a04d08
        if args.config:
Packit Service a04d08
            cfg = util.read_conf(args.config)
Packit Service a04d08
            if 'datasource' in cfg:
Packit Service a04d08
                cfg = cfg['datasource']['MAAS']
Packit Service a04d08
            for key in creds.keys():
Packit Service a04d08
                if key in cfg and creds[key] is None:
Packit Service a04d08
                    creds[key] = cfg[key]
Packit Service a04d08
            if args.url is None and 'metadata_url' in cfg:
Packit Service a04d08
                args.url = cfg['metadata_url']
Packit Service a04d08
Packit Service a04d08
        if args.url is None:
Packit Service a04d08
            sys.stderr.write("Must provide a url or a config with url.\n")
Packit Service a04d08
            sys.exit(1)
Packit Service a04d08
Packit Service a04d08
        oauth_helper = get_oauth_helper(creds)
Packit Service a04d08
Packit Service a04d08
        def geturl(url):
Packit Service a04d08
            # the retry is to ensure that oauth timestamp gets fixed
Packit Service a04d08
            return oauth_helper.readurl(url, retries=1).contents
Packit Service a04d08
Packit Service a04d08
        def printurl(url):
Packit Service a04d08
            print("== %s ==\n%s\n" % (url, geturl(url).decode()))
Packit Service a04d08
Packit Service a04d08
        def crawl(url):
Packit Service a04d08
            if url.endswith("/"):
Packit Service a04d08
                for line in geturl(url).decode().splitlines():
Packit Service a04d08
                    if line.endswith("/"):
Packit Service a04d08
                        crawl("%s%s" % (url, line))
Packit Service a04d08
                    elif line == "meta-data":
Packit Service a04d08
                        # meta-data is a dir, it *should* end in a /
Packit Service a04d08
                        crawl("%s%s" % (url, "meta-data/"))
Packit Service a04d08
                    else:
Packit Service a04d08
                        printurl("%s%s" % (url, line))
Packit Service a04d08
            else:
Packit Service a04d08
                printurl(url)
Packit Service a04d08
Packit Service a04d08
        if args.subcmd == "check-seed":
Packit Service a04d08
            sys.stderr.write("Checking seed at %s\n" % args.url)
Packit Service a04d08
            readurl = oauth_helper.readurl
Packit Service a04d08
            if args.url[0] == "/" or args.url.startswith("file://"):
Packit Service a04d08
                (userdata, metadata, vd) = read_maas_seed_dir(args.url)
Packit Service a04d08
            else:
Packit Service a04d08
                (userdata, metadata, vd) = read_maas_seed_url(
Packit Service a04d08
                    args.url, version=args.apiver, read_file_or_url=readurl,
Packit Service a04d08
                    retries=2)
Packit Service a04d08
            print("=== user-data ===")
Packit Service a04d08
            print("N/A" if userdata is None else userdata.decode())
Packit Service a04d08
            print("=== meta-data ===")
Packit Service a04d08
            pprint.pprint(metadata)
Packit Service a04d08
            print("=== vendor-data ===")
Packit Service a04d08
            pprint.pprint("N/A" if vd is None else vd)
Packit Service a04d08
Packit Service a04d08
        elif args.subcmd == "get":
Packit Service a04d08
            printurl(args.url)
Packit Service a04d08
Packit Service a04d08
        elif args.subcmd == "crawl":
Packit Service a04d08
            if not args.url.endswith("/"):
Packit Service a04d08
                args.url = "%s/" % args.url
Packit Service a04d08
            crawl(args.url)
Packit Service a04d08
Packit Service a04d08
    main()
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab