← Back to team overview

cloud-init-dev team mailing list archive

Re: [Merge] ~smoser/cloud-init:feature/datasource-ibmcloud into cloud-init:master

 

some inline comments, questions.

Diff comments:

> diff --git a/cloudinit/sources/DataSourceIBMCloud.py b/cloudinit/sources/DataSourceIBMCloud.py
> new file mode 100644
> index 0000000..99ec4c2
> --- /dev/null
> +++ b/cloudinit/sources/DataSourceIBMCloud.py
> @@ -0,0 +1,275 @@
> +# This file is part of cloud-init. See LICENSE file for license information.
> +"""Datasource for IBMCloud.
> +
> +IBMCloud is also know as SoftLayer or BlueMix.
> +IBMCloud hypervisor is xen (2018-03-10).
> +
> +There are 2 different api exposed launch methods.
> + * template: This is the legacy method of launching instances.
> +   When booting from an image template, the system boots first into
> +   a "provisioning" mode.  There, host <-> guest mechanisms are utilized
> +   to execute code in the guest and provision it.
> +
> +   Cloud-init will disables itself when it detects that it is in the
> +   provisioning mode.  It detects this by the presence of
> +   a file '/root/provisioningConfiguration.cfg'.
> +
> +   When provided with user-data, the "first boot" will contain a
> +   ConfigDrive-like disk labeled with 'METADATA'.  If there is no user-data
> +   provided, then there is no data-source.
> +
> +   Cloud-init never does any network configuration in this mode.
> +
> + * os_code: Essentially "launch by OS Code" (Operating System Code).
> +   This is a more modern approach.  There is no specific "provisioning" boot.
> +   Instead, cloud-init does all the customization.  With or without
> +   user-data provided, an OpenStack ConfigDrive is attached.
> +
> +   This disk will have only a 'latest' version, and will have the UUID
> +   of 9796-932E.
> +
> +TODO:
> + * is uuid (/sys/hypervisor/uuid) stable for life of an instance?
> +   it seems it is not the same as data's uuid in the os_code case
> +   but is in the template case.
> +
> +"""
> +import os
> +
> +from cloudinit import log as logging
> +from cloudinit import sources
> +from cloudinit.sources.helpers import openstack
> +from cloudinit import util
> +
> +LOG = logging.getLogger(__name__)
> +
> +IBM_CONFIG_UUID = "9796-932E"
> +
> +
> +class Platforms(object):
> +    TEMPLATE_LIVE_METADATA = "Template/Live/Metadata"
> +    TEMPLATE_LIVE_NODATA = "UNABLE TO BE IDENTIFIED."
> +    TEMPLATE_PROVISIONING_METADATA = "Template/Provisioning/Metadata"
> +    TEMPLATE_PROVISIONING_NODATA = "Template/Provisioning/No-Metadata"
> +    OS_CODE = "OS-Code/Live"
> +
> +
> +PROVISIONING = (
> +    Platforms.TEMPLATE_PROVISIONING_METADATA,
> +    Platforms.TEMPLATE_PROVISIONING_NODATA)
> +
> +
> +class DataSourceIBMCloud(sources.DataSource):
> +
> +    dsname = 'IBMCloud'
> +    system_uuid = None

did we want to initilize this to "unset" instead of None w.r.t restoring objects?

> +
> +    def __init__(self, sys_cfg, distro, paths):
> +        super(DataSourceIBMCloud, self).__init__(sys_cfg, distro, paths)
> +        self.source = None
> +        self._network_config = None
> +        self.network_json = None
> +        self.platform = None
> +
> +    def __str__(self):
> +        root = sources.DataSource.__str__(self)
> +        mstr = "%s [%s %s]" % (root, self.platform, self.source)
> +        return mstr
> +
> +    def _get_data(self):
> +        results = read_md()
> +        if results is None:
> +            return False
> +
> +        self.source = results['source']
> +        self.platform = results['platform']
> +        self.metadata = results['metadata']
> +        self.userdata_raw = results.get('userdata')
> +        self.version = results['version']
> +        self.network_json = results.get('networkdata')
> +        vd = results.get('vendordata')
> +        self.vendordata_pure = vd
> +        self.system_uuid = results['system-uuid']
> +        try:
> +            self.vendordata_raw = sources.convert_vendordata(vd)
> +        except ValueError as e:
> +            LOG.warning("Invalid content in vendor-data: %s", e)
> +            self.vendordata_raw = None
> +
> +        return True
> +
> +    def check_instance_id(self, sys_cfg):
> +        """quickly (local check only) if self.instance_id is still valid
> +
> +        in Template mode, the system uuid (/sys/hypervisor/uuid) is the
> +        same as found in the METADATA disk.  But that is not true in OS_CODE
> +        mode.  So we read the system_uuid and keep that for later compare."""
> +        if self.system_uuid is None:
> +            return False
> +        return self.system_uuid == _read_system_uuid()
> +
> +    @property
> +    def network_config(self):
> +        if self.platform != Platforms.OS_CODE:
> +            # If deployed from template, an agent in the provisioning
> +            # environment handles networking configuration. Not cloud-init.
> +            return {'config': 'disabled', 'version': 1}
> +        if self._network_config is None:
> +            if self.network_json is not None:
> +                LOG.debug("network config provided via network_json")
> +                self._network_config = openstack.convert_net_json(
> +                    self.network_json, known_macs=None)
> +            else:
> +                LOG.debug("no network configuration available.")
> +        return self._network_config
> +
> +
> +def _read_system_uuid():
> +    uuid_path = "/sys/hypervisor/uuid"
> +    if not os.path.isfile(uuid_path):
> +        return None
> +    return util.load_file(uuid_path).strip().lower()
> +
> +
> +def _is_xen():
> +    return os.path.exists("/sys/hypervisor")
> +
> +
> +def _is_ibm_provisioning():
> +    return os.path.exists("/root/provisioningConfiguration.cfg")
> +
> +
> +def get_ibm_platform():
> +    """Return a tuple (Platform, path)
> +
> +    If this is Not IBM cloud, then the return value is (None, None).
> +    An instance in provisioning mode is considered running on IBM cloud."""
> +    label_mdata = "METADATA"
> +    label_cfg2 = "CONFIG-2"
> +    not_found = (None, None)
> +
> +    if not _is_xen():
> +        return not_found
> +
> +    # fslabels contains only the first entry with a given label.
> +    fslabels = {}
> +    try:
> +        devs = util.blkid()
> +    except util.ProcessExecutionError as e:
> +        LOG.warning("Failed to run blkid: %s", e)
> +        return (None, None)
> +
> +    for dev in sorted(devs.keys()):
> +        data = devs[dev]
> +        label = data.get("LABEL", "").upper()
> +        uuid = data.get("UUID", "").upper()
> +        if label not in (label_mdata, label_cfg2):
> +            continue
> +        if label in fslabels:
> +            LOG.warning("Duplicate fslabel '%s'. existing=%s current=%s",
> +                        label, fslabels[label], data)
> +            continue
> +        if label == label_cfg2 and uuid != IBM_CONFIG_UUID:
> +            LOG.debug("Skipping %s with LABEL=%s due to uuid != %s: %s",
> +                      dev, label, uuid, data)
> +            continue
> +        fslabels[label] = data
> +
> +    metadata_path = fslabels.get(label_mdata, {}).get('DEVNAME')
> +    cfg2_path = fslabels.get(label_cfg2, {}).get('DEVNAME')
> +
> +    if cfg2_path:
> +        return (Platforms.OS_CODE, cfg2_path)
> +    elif metadata_path:
> +        if _is_ibm_provisioning():
> +            return (Platforms.TEMPLATE_PROVISIONING_METADATA, metadata_path)
> +        else:
> +            return (Platforms.TEMPLATE_LIVE_METADATA, metadata_path)
> +    elif _is_ibm_provisioning():
> +            return (Platforms.TEMPLATE_PROVISIONING_NODATA, None)
> +    return not_found
> +
> +
> +def read_md():
> +    """Read data from IBM Cloud.
> +
> +    @return: None if not running on IBM Cloud.
> +             dictionary with guaranteed fields: metadata, version
> +             and optional fields: userdata, vendordata, networkdata.
> +             Also includes the system uuid from /sys/hypervisor/uuid."""
> +    platform, path = get_ibm_platform()
> +    if platform is None:
> +        LOG.debug("This is not an IBMCloud platform.")
> +        return None
> +    elif platform in PROVISIONING:
> +        LOG.debug("Cloud-init is disabled during provisioning: %s.",
> +                  platform)
> +        return None
> +
> +    ret = {'platform': platform, 'source': path}
> +
> +    try:
> +        if os.path.isdir(path):
> +            results = read_config_drive(path)
> +        else:
> +            results = util.mount_cb(path, read_config_drive)
> +    except Exception as e:
> +        raise RuntimeError(
> +            "Failed mounting IBM config disk. platform=%s path=%s: %s" %
> +            (platform, path, e))
> +
> +    ret['system-uuid'] = _read_system_uuid()
> +
> +    # rename public_keys so it can be read by base class get_public_ssh_keys
> +    md = results['metadata']
> +    renames = (('public_keys', 'public-keys'), ('hostname', 'local-hostname'))
> +    for mdname, newname in renames:
> +        if mdname in md:
> +            md[newname] = md[mdname]
> +            del md[mdname]
> +    for drop in ('name', 'uuid', 'files', 'network_config'):
> +        if drop in md:
> +            del md[drop]
> +
> +    if results.get('userdata') == "":
> +        # the read_v2 incorrectly puts user-data in as ''.
> +        # this work around does cuase issue if user provided user-data as "".
> +        results['userdata'] = None
> +
> +    # In order to avoid additional functionality being utilized from the
> +    # copied openstack interface we specifically pull out only some fields.
> +    # in the read_v2 response.  Specifically avoided are
> +    #   'files', 'ec2-metadata', 'dsmode', 'network_config'
> +    # network_config is ENI formatted data that is legacy.  It may be
> +    # present in a Template environment, but we assume the provision
> +    # stage handles that configuration.
> +    allowed = ('metadata', 'userdata', 'vendordata', 'version', 'networkdata')
> +    ret.update(dict([(k, v) for k, v in results.items() if k in allowed]))
> +    return ret
> +
> +
> +def read_config_drive(source_dir):
> +    reader = openstack.ConfigDriveReader(source_dir, versions=["latest"])
> +    return reader.read_v2()
> +
> +
> +# Used to match classes to dependencies
> +datasources = [
> +    (DataSourceIBMCloud, (sources.DEP_FILESYSTEM,)),
> +]
> +
> +
> +# Return a list of data sources that match this set of dependencies
> +def get_datasource_list(depends):
> +    return sources.list_from_depends(depends, datasources)
> +
> +
> +if __name__ == "__main__":
> +    import argparse
> +
> +    parser = argparse.ArgumentParser(description='Query IBM Cloud Metadata')
> +    args = parser.parse_args()
> +    data = read_md()
> +    print(util.json_dumps(data))
> +
> +# vi: ts=4 expandtab
> diff --git a/cloudinit/util.py b/cloudinit/util.py
> index cae8b19..7ae62cc 100644
> --- a/cloudinit/util.py
> +++ b/cloudinit/util.py
> @@ -1237,6 +1237,30 @@ def find_devs_with(criteria=None, oformat='device',
>      return entries
>  
>  
> +def blkid(devs=None, disable_cache=False):

Shouldn't we update util.find_devs_with() to make use of this? or some other refactoring
since we now have to callers to blkid?

> +    if devs is None:
> +        devs = []
> +    else:
> +        devs = list(devs)
> +
> +    cmd = ['blkid', '-o', 'full']
> +    if disable_cache:
> +        cmd.extend(['-c', '/dev/null'])
> +    cmd.extend(devs)
> +
> +    # we have to decode with 'replace' as shelx.split (called by
> +    # load_shell_content) can't take bytes.  So this is potentially
> +    # lossy of non-utf-8 chars in blkid output.
> +    out, _ = subp(cmd, capture=True, decode="replace")
> +    ret = {}
> +    for line in out.splitlines():
> +        dev, _, data = line.partition(":")
> +        ret[dev] = load_shell_content(data)
> +        ret[dev]["DEVNAME"] = dev
> +
> +    return ret
> +
> +
>  def peek_file(fname, max_bytes):
>      LOG.debug("Peeking at %s (max_bytes=%s)", fname, max_bytes)
>      with open(fname, 'rb') as ifh:
> diff --git a/tests/unittests/test_datasource/test_ibmcloud.py b/tests/unittests/test_datasource/test_ibmcloud.py
> new file mode 100644
> index 0000000..f6f5419
> --- /dev/null
> +++ b/tests/unittests/test_datasource/test_ibmcloud.py

Don't we want to put this in cloudinit/sources/tests/test_ibmcloud.py ?

> @@ -0,0 +1,258 @@
> +# This file is part of cloud-init. See LICENSE file for license information.
> +
> +from cloudinit.sources import DataSourceIBMCloud as ibm
> +from cloudinit.tests import helpers as test_helpers
> +
> +import base64
> +import copy
> +import json
> +import mock
> +from textwrap import dedent
> +
> +D_PATH = "cloudinit.sources.DataSourceIBMCloud."
> +
> +
> +class TestIBMCloud(test_helpers.CiTestCase):
> +    """Test the datasource."""
> +    def setUp(self):
> +        super(TestIBMCloud, self).setUp()
> +        pass
> +
> +
> +@mock.patch(D_PATH + "_is_xen", return_value=True)
> +@mock.patch(D_PATH + "_is_ibm_provisioning")
> +@mock.patch(D_PATH + "util.blkid")
> +class TestGetIBMPlatform(test_helpers.CiTestCase):
> +    """Test the get_ibm_platform helper."""
> +
> +    blkid_base = {
> +        "/dev/xvda1": {
> +            "DEVNAME": "/dev/xvda1", "LABEL": "cloudimg-bootfs",
> +            "TYPE": "ext3"},
> +        "/dev/xvda2": {
> +            "DEVNAME": "/dev/xvda2", "LABEL": "cloudimg-rootfs",
> +            "TYPE": "ext4"},
> +    }
> +
> +    blkid_metadata_disk = {
> +        "/dev/xvdh1": {
> +            "DEVNAME": "/dev/xvdh1", "LABEL": "METADATA", "TYPE": "vfat",
> +            "SEC_TYPE": "msdos", "UUID": "681B-8C5D",
> +            "PARTUUID": "3d631e09-01"},
> +    }
> +
> +    blkid_oscode_disk = {
> +        "/dev/xvdh": {
> +            "DEVNAME": "/dev/xvdh", "LABEL": "config-2", "TYPE": "vfat",
> +            "SEC_TYPE": "msdos", "UUID": ibm.IBM_CONFIG_UUID}
> +    }
> +
> +    def setUp(self):
> +        self.blkid_metadata = copy.deepcopy(self.blkid_base)

aren't the copies only needed in the test-cases that modify the base config?  You could defer the copy to those unittests

> +        self.blkid_metadata.update(copy.deepcopy(self.blkid_metadata_disk))
> +
> +        self.blkid_oscode = copy.deepcopy(self.blkid_base)
> +        self.blkid_oscode.update(copy.deepcopy(self.blkid_oscode_disk))
> +
> +    def test_id_template_live_metadata(self, m_blkid, m_is_prov, _m_xen):
> +        """identify TEMPLATE_LIVE_METADATA."""
> +        m_blkid.return_value = self.blkid_metadata
> +        m_is_prov.return_value = False
> +        self.assertEqual(
> +            (ibm.Platforms.TEMPLATE_LIVE_METADATA, "/dev/xvdh1"),
> +            ibm.get_ibm_platform())
> +
> +    def test_id_template_prov_metadata(self, m_blkid, m_is_prov, _m_xen):
> +        """identify TEMPLATE_PROVISIONING_METADATA."""
> +        m_blkid.return_value = self.blkid_metadata
> +        m_is_prov.return_value = True
> +        self.assertEqual(
> +            (ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh1"),
> +            ibm.get_ibm_platform())
> +
> +    def test_id_template_prov_nodata(self, m_blkid, m_is_prov, _m_xen):
> +        """identify TEMPLATE_PROVISIONING_NODATA."""
> +        m_blkid.return_value = self.blkid_base
> +        m_is_prov.return_value = True
> +        self.assertEqual(
> +            (ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None),
> +            ibm.get_ibm_platform())
> +
> +    def test_id_os_code(self, m_blkid, m_is_prov, _m_xen):
> +        """Identify OS_CODE."""
> +        m_blkid.return_value = self.blkid_oscode
> +        m_is_prov.return_value = False
> +        self.assertEqual((ibm.Platforms.OS_CODE, "/dev/xvdh"),
> +                         ibm.get_ibm_platform())
> +
> +    def test_id_os_code_must_match_uuid(self, m_blkid, m_is_prov, _m_xen):
> +        """Test against false positive on openstack with non-ibm UUID."""
> +        blkid = self.blkid_oscode
> +        blkid["/dev/xvdh"]["UUID"] = "9999-9999"
> +        m_blkid.return_value = blkid
> +        m_is_prov.return_value = False
> +        self.assertEqual((None, None), ibm.get_ibm_platform())
> +
> +
> +@mock.patch(D_PATH + "_read_system_uuid", return_value=None)
> +@mock.patch(D_PATH + "get_ibm_platform")
> +class TestReadMD(test_helpers.CiTestCase):
> +    """Test the read_datasource helper."""
> +
> +    template_md = {
> +        "files": [],
> +        "network_config": {"content_path": "/content/interfaces"},
> +        "hostname": "ci-fond-ram",
> +        "name": "ci-fond-ram",
> +        "domain": "testing.ci.cloud-init.org",
> +        "meta": {"dsmode": "net"},
> +        "uuid": "8e636730-9f5d-c4a5-327c-d7123c46e82f",
> +        "public_keys": {"1091307": "ssh-rsa AAAAB3NzaC1...Hw== ci-pubkey"},
> +    }
> +
> +    oscode_md = {
> +        "hostname": "ci-grand-gannet.testing.ci.cloud-init.org",
> +        "name": "ci-grand-gannet",
> +        "uuid": "2f266908-8e6c-4818-9b5c-42e9cc66a785",
> +        "random_seed": "bm90LXJhbmRvbQo=",
> +        "crypt_key": "ssh-rsa AAAAB3NzaC1yc2..n6z/",
> +        "configuration_token": "eyJhbGciOi..M3ZA",
> +        "public_keys": {"1091307": "ssh-rsa AAAAB3N..Hw== ci-pubkey"},
> +    }
> +
> +    content_interfaces = dedent("""\
> +        auto lo
> +        iface lo inet loopback
> +
> +        auto eth0
> +        allow-hotplug eth0
> +        iface eth0 inet static
> +        address 10.82.43.5
> +        netmask 255.255.255.192
> +        """)
> +
> +    userdata = b"#!/bin/sh\necho hi mom\n"
> +    # meta.js file gets json encoded userdata as a list.
> +    meta_js = '["#!/bin/sh\necho hi mom\n"]'
> +    vendor_data = {
> +        "cloud-init": "#!/bin/bash\necho 'root:$6$5ab01p1m1' | chpasswd -e"}
> +
> +    network_data = {
> +        "links": [
> +            {"id": "interface_29402281", "name": "eth0", "mtu": None,
> +             "type": "phy", "ethernet_mac_address": "06:00:f1:bd:da:25"},
> +            {"id": "interface_29402279", "name": "eth1", "mtu": None,
> +             "type": "phy", "ethernet_mac_address": "06:98:5e:d0:7f:86"}
> +        ],
> +        "networks": [
> +            {"id": "network_109887563", "link": "interface_29402281",
> +             "type": "ipv4", "ip_address": "10.82.43.2",
> +             "netmask": "255.255.255.192",
> +             "routes": [
> +                 {"network": "10.0.0.0", "netmask": "255.0.0.0",
> +                  "gateway": "10.82.43.1"},
> +                 {"network": "161.26.0.0", "netmask": "255.255.0.0",
> +                  "gateway": "10.82.43.1"}]},
> +            {"id": "network_109887551", "link": "interface_29402279",
> +             "type": "ipv4", "ip_address": "108.168.194.252",
> +             "netmask": "255.255.255.248",
> +             "routes": [
> +                 {"network": "0.0.0.0", "netmask": "0.0.0.0",
> +                  "gateway": "108.168.194.249"}]}
> +        ],
> +        "services": [
> +            {"type": "dns", "address": "10.0.80.11"},
> +            {"type": "dns", "address": "10.0.80.12"}
> +        ],
> +    }
> +
> +    sysuuid = '7f79ebf5-d791-43c3-a723-854e8389d59f'
> +
> +    def test_provisioning_md(self, m_platform, m_sysuuid):
> +        m_platform.return_value = (

These tests need docstrings

> +            ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh")
> +        self.assertIsNone(ibm.read_md())
> +
> +    def test_provisioning_no_metadata(self, m_platform, m_sysuuid):
> +        m_platform.return_value = (
> +            ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None)
> +        self.assertIsNone(ibm.read_md())
> +
> +    def test_provisioning_not_ibm(self, m_platform, m_sysuuid):
> +        m_platform.return_value = (None, None)
> +        self.assertIsNone(ibm.read_md())
> +
> +    def _get_expected_metadata(self, os_md):
> +        """return expected 'metadata' for data loaded fro meta_data.json."""
> +        os_md = copy.deepcopy(os_md)
> +        copies = ('meta', 'domain', 'configuration_token', 'crypt_key')
> +        renames = (
> +            ('hostname', 'local-hostname'),
> +            ('uuid', 'instance-id'),
> +            ('public_keys', 'public-keys'))
> +        ret = {}
> +        ret.update(dict([(k, os_md[k]) for k in copies if k in os_md]))
> +        for osname, mdname in renames:
> +            if osname in os_md:
> +                ret[mdname] = os_md[osname]
> +        if 'random_seed' in os_md:
> +            ret['random_seed'] = base64.b64decode(os_md['random_seed'])
> +
> +        return ret
> +
> +    def test_template_live(self, m_platform, m_sysuuid):
> +        tmpdir = self.tmp_dir()
> +        m_platform.return_value = (
> +            ibm.Platforms.TEMPLATE_LIVE_METADATA, tmpdir)
> +        m_sysuuid.return_value = self.sysuuid
> +
> +        test_helpers.populate_dir(tmpdir, {
> +            'openstack/latest/meta_data.json': json.dumps(self.template_md),
> +            'openstack/latest/user_data': self.userdata,
> +            'openstack/content/interfaces': self.content_interfaces,
> +            'meta.js': self.meta_js})
> +
> +        ret = ibm.read_md()
> +        self.assertEqual(ibm.Platforms.TEMPLATE_LIVE_METADATA,
> +                         ret['platform'])
> +        self.assertEqual(tmpdir, ret['source'])
> +        self.assertEqual(self.userdata, ret['userdata'])
> +        self.assertEqual(self._get_expected_metadata(self.template_md),
> +                         ret['metadata'])
> +        self.assertEqual(self.sysuuid, ret['system-uuid'])
> +
> +    def test_os_code_live(self, m_platform, m_sysuuid):
> +        """verify an os_code metadata path."""
> +        tmpdir = self.tmp_dir()
> +        m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir)
> +        test_helpers.populate_dir(tmpdir, {
> +            'openstack/latest/meta_data.json': json.dumps(self.oscode_md),
> +            'openstack/latest/user_data': self.userdata,
> +            'openstack/latest/vendor_data.json': json.dumps(self.vendor_data),
> +        })
> +
> +        ret = ibm.read_md()
> +        self.assertEqual(ibm.Platforms.OS_CODE, ret['platform'])
> +        self.assertEqual(tmpdir, ret['source'])
> +        self.assertEqual(self.userdata, ret['userdata'])
> +        self.assertEqual(self._get_expected_metadata(self.oscode_md),
> +                         ret['metadata'])
> +
> +    def test_os_code_live_no_userdata(self, m_platform, m_sysuuid):
> +        """Verify os_code without user-data."""
> +        tmpdir = self.tmp_dir()
> +        m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir)
> +        test_helpers.populate_dir(tmpdir, {
> +            'openstack/latest/meta_data.json': json.dumps(self.oscode_md),
> +            'openstack/latest/vendor_data.json': json.dumps(self.vendor_data),
> +        })
> +
> +        ret = ibm.read_md()
> +        self.assertEqual(ibm.Platforms.OS_CODE, ret['platform'])
> +        self.assertEqual(tmpdir, ret['source'])
> +        self.assertIsNone(ret['userdata'])
> +        self.assertEqual(self._get_expected_metadata(self.oscode_md),
> +                         ret['metadata'])
> +
> +
> +# vi: ts=4 expandtab
> diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
> index 9c5628e..f3f0b4c 100644
> --- a/tests/unittests/test_ds_identify.py
> +++ b/tests/unittests/test_ds_identify.py
> @@ -37,8 +39,8 @@ BLKID_UEFI_UBUNTU = [
>  
>  POLICY_FOUND_ONLY = "search,found=all,maybe=none,notfound=disabled"
>  POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=all,notfound=disabled"
> -DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=enabled"
> -DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=disabled"
> +DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=disabled"
> +DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=enabled"

why is this changing?  Should we read this from the source tree?

>  DI_EC2_STRICT_ID_DEFAULT = "true"
>  OVF_MATCH_STRING = 'http://schemas.dmtf.org/ovf/environment/1'
>  


-- 
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/341774
Your team cloud-init commiters is requested to review the proposed merge of ~smoser/cloud-init:feature/datasource-ibmcloud into cloud-init:master.


References