Chad Smith has proposed merging ~chad.smith/cloud-init:azure-di-id-asset-tag 
into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/324875

azure: ds-identify and Datasource.get_data checks DMI chassis-asset-tag


Azure sets a known chassis asset tag to 7783-7084-3265-9085-8269-3286-77. We 
can inspect this in both ds-identify and DataSource.get_data to determine 
whether we are on Azure. Added unit tests to cover these changes and some minor 
tweaks to Exception error message content to give more context on malformed or 
missing ovf-env.xml files.

LP: #1693939
-- 
Your team cloud-init commiters is requested to review the proposed merge of 
~chad.smith/cloud-init:azure-di-id-asset-tag into cloud-init:master.
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index b9458ff..9dc9157 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -36,6 +36,8 @@ RESOURCE_DISK_PATH = '/dev/disk/cloud/azure_resource'
 DEFAULT_PRIMARY_NIC = 'eth0'
 LEASE_FILE = '/var/lib/dhcp/dhclient.eth0.leases'
 DEFAULT_FS = 'ext4'
+# DMI chassis-asset-tag is set static for all azure instances
+AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
 
 
 def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):
@@ -319,7 +321,11 @@ class DataSourceAzureNet(sources.DataSource):
     def get_data(self):
         # azure removes/ejects the cdrom containing the ovf-env.xml
         # file on reboot.  So, in order to successfully reboot we
-        # need to look in the datadir and consider that valid
+        # need to look in the datadir and consider that valida
+        asset_tag = util.read_dmi_data('chassis-asset-tag')
+        if asset_tag != AZURE_CHASSIS_ASSET_TAG:
+            LOG.info("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
+            return False
         ddir = self.ds_cfg['data_dir']
 
         candidates = [self.seed_dir]
@@ -694,7 +700,7 @@ def read_azure_ovf(contents):
     try:
         dom = minidom.parseString(contents)
     except Exception as e:
-        raise BrokenAzureDataSource("invalid xml: %s" % e)
+        raise BrokenAzureDataSource("Invalid ovf-env.xml: %s" % e)
 
     results = find_child(dom.documentElement,
                          lambda n: n.localName == "ProvisioningSection")
@@ -817,7 +823,8 @@ def load_azure_ds_dir(source_dir):
     ovf_file = os.path.join(source_dir, "ovf-env.xml")
 
     if not os.path.isfile(ovf_file):
-        raise NonAzureDataSource("No ovf-env file found")
+        raise NonAzureDataSource(
+            "No ovf-env.xml file found at %s" % (source_dir))
 
     with open(ovf_file, "rb") as fp:
         contents = fp.read()
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 852ec70..f42d9c2 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -76,7 +76,9 @@ def construct_valid_ovf_env(data=None, pubkeys=None, userdata=None):
     return content
 
 
-class TestAzureDataSource(TestCase):
+class TestAzureDataSource(CiTestCase):
+
+    with_logs = True
 
     def setUp(self):
         super(TestAzureDataSource, self).setUp()
@@ -160,6 +162,12 @@ scbus-1 on xpt0 bus 0
 
         self.instance_id = 'test-instance-id'
 
+        def _dmi_mocks(key):
+            if key == 'system-uuid':
+                return self.instance_id
+            elif key == 'chassis-asset-tag':
+                return '7783-7084-3265-9085-8269-3286-77'
+
         self.apply_patches([
             (dsaz, 'list_possible_azure_ds_devs', dsdevs),
             (dsaz, 'invoke_agent', _invoke_agent),
@@ -170,7 +178,7 @@ scbus-1 on xpt0 bus 0
             (dsaz, 'set_hostname', mock.MagicMock()),
             (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
             (dsaz.util, 'read_dmi_data', mock.MagicMock(
-                return_value=self.instance_id)),
+                side_effect=_dmi_mocks)),
         ])
 
         dsrc = dsaz.DataSourceAzureNet(
@@ -241,6 +249,23 @@ fdescfs            /dev/fd          fdescfs rw              0 0
             res = get_path_dev_freebsd('/etc', mnt_list)
             self.assertIsNotNone(res)
 
+    @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')
+    def test_non_azure_dmi_chassis_asset_tag(self, m_read_dmi_data):
+        """Report non-azure when DMI's chassis asset tag doesn't match.
+
+        Return False when the asset tag doesn't match Azure's static
+        AZURE_CHASSIS_ASSET_TAG.
+        """
+        # Return a non-matching asset tag value
+        nonazure_tag = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'
+        m_read_dmi_data.return_value = nonazure_tag
+        dsrc = dsaz.DataSourceAzureNet(
+            {}, distro=None, paths=self.paths)
+        self.assertFalse(dsrc.get_data())
+        self.assertEqual(
+            "Non-Azure DMI asset tag '{0}' discovered.\n".format(nonazure_tag),
+            self.logs.getvalue())
+
     def test_basic_seed_dir(self):
         odata = {'HostName': "myhost", 'UserName': "myuser"}
         data = {'ovfcontent': construct_valid_ovf_env(data=odata),
@@ -696,6 +721,33 @@ class TestAzureBounce(TestCase):
         self.assertEqual(0, self.set_hostname.call_count)
 
 
+class TestLoadAzureDsDir(CiTestCase):
+    """Tests for load_azure_ds_dir."""
+
+    def setUp(self):
+        self.source_dir = self.tmp_dir()
+        super(TestLoadAzureDsDir, self).setUp()
+
+    def test_missing_ovf_env_xml_raises_non_azure_datasource_error(self):
+        """load_azure_ds_dir raises an error When ovf-env.xml doesn't exit."""
+        with self.assertRaises(dsaz.NonAzureDataSource) as context_manager:
+            dsaz.load_azure_ds_dir(self.source_dir)
+        self.assertEqual(
+            'No ovf-env.xml file found at {0}'.format(self.source_dir),
+            str(context_manager.exception))
+
+    def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self):
+        """load_azure_ds_dir calls read_azure_ovf to parse the xml."""
+        ovf_path = os.path.join(self.source_dir, 'ovf-env.xml')
+        with open(ovf_path, 'wb') as stream:
+            stream.write('invalid xml')
+        with self.assertRaises(dsaz.BrokenAzureDataSource) as context_manager:
+            dsaz.load_azure_ds_dir(self.source_dir)
+        self.assertEqual(
+            'Invalid ovf-env.xml: syntax error: line 1, column 0',
+            str(context_manager.exception))
+
+
 class TestReadAzureOvf(TestCase):
     def test_invalid_xml_raises_non_azure_ds(self):
         invalid_xml = "<foo>" + construct_valid_ovf_env(data={})
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
index 5c26e65..31335ac 100644
--- a/tests/unittests/test_ds_identify.py
+++ b/tests/unittests/test_ds_identify.py
@@ -39,9 +39,11 @@ RC_FOUND = 0
 RC_NOT_FOUND = 1
 DS_NONE = 'None'
 
+P_CHASSIS_ASSET_TAG = "sys/class/dmi/id/chassis_asset_tag"
 P_PRODUCT_NAME = "sys/class/dmi/id/product_name"
 P_PRODUCT_SERIAL = "sys/class/dmi/id/product_serial"
 P_PRODUCT_UUID = "sys/class/dmi/id/product_uuid"
+P_SEED_DIR = "var/lib/cloud/seed"
 P_DSID_CFG = "etc/cloud/ds-identify.cfg"
 
 MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}
@@ -160,6 +162,16 @@ class TestDsIdentify(CiTestCase):
                 _print_run_output(rc, out, err, cfg, files)
         return rc, out, err, cfg, files
 
+    def test_azure_dmi_detection_from_chassis_asset_tag(self):
+        """Azure datasource is detected from DMI chassis-asset-tag"""
+        self._test_ds_found('Azure-dmi-detection')
+
+    def test_azure_seed_file_detection(self):
+        """Azure datasource is detected due to presence of a seed file.
+
+        The seed file tested  is /var/lib/cloud/seed/azure/ovf-env.xml."""
+        self._test_ds_found('Azure-seed-detection')
+
     def test_aws_ec2_hvm(self):
         """EC2: hvm instances use dmi serial and uuid starting with 'ec2'."""
         self._test_ds_found('Ec2-hvm')
@@ -268,9 +280,24 @@ def _print_run_output(rc, out, err, cfg, files):
 
 
 VALID_CFG = {
+<<<<<<< tests/unittests/test_ds_identify.py
     'AliYun': {
         'ds': 'AliYun',
         'files': {P_PRODUCT_NAME: 'Alibaba Cloud ECS\n'},
+=======
+    'Azure-dmi-detection': {
+        'ds': 'Azure',
+        'files': {
+            P_CHASSIS_ASSET_TAG: '7783-7084-3265-9085-8269-3286-77\n',
+        }
+    },
+    'Azure-seed-detection': {
+        'ds': 'Azure',
+        'files': {
+            P_CHASSIS_ASSET_TAG: 'No-match\n',
+            os.path.join(P_SEED_DIR, 'azure', 'ovf-env.xml'): 'present\n',
+        }
+>>>>>>> tests/unittests/test_ds_identify.py
     },
     'Ec2-hvm': {
         'ds': 'Ec2',
diff --git a/tools/ds-identify b/tools/ds-identify
index 5fc500b..12633c7 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -85,6 +85,7 @@ DI_MAIN=${DI_MAIN:-main}
 
 DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
 DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
+DI_DMI_CHASSIS_ASSET_TAG=""
 DI_DMI_PRODUCT_NAME=""
 DI_DMI_SYS_VENDOR=""
 DI_DMI_PRODUCT_SERIAL=""
@@ -259,6 +260,12 @@ read_kernel_cmdline() {
     DI_KERNEL_CMDLINE="$cmdline"
 }
 
+read_dmi_chassis_asset_tag() {
+    cached "${DI_DMI_CHASSIS_ASSET_TAG}" && return
+    get_dmi_field chassis_asset_tag
+    DI_DMI_CHASSIS_ASSET_TAG="$_RET"
+}
+
 read_dmi_sys_vendor() {
     cached "${DI_DMI_SYS_VENDOR}" && return
     get_dmi_field sys_vendor
@@ -386,6 +393,14 @@ read_pid1_product_name() {
     DI_PID_1_PRODUCT_NAME="$product_name"
 }
 
+dmi_chassis_asset_tag_matches() {
+    is_container && return 1
+    case "${DI_DMI_CHASSIS_ASSET_TAG}" in
+        $1) return 0;;
+    esac
+    return 1
+}
+
 dmi_product_name_matches() {
     is_container && return 1
     case "${DI_DMI_PRODUCT_NAME}" in
@@ -402,11 +417,6 @@ dmi_product_serial_matches() {
     return 1
 }
 
-dmi_product_name_is() {
-    is_container && return 1
-    [ "${DI_DMI_PRODUCT_NAME}" = "$1" ]
-}
-
 dmi_sys_vendor_is() {
     is_container && return 1
     [ "${DI_DMI_SYS_VENDOR}" = "$1" ]
@@ -478,7 +488,7 @@ dscheck_CloudStack() {
 
 dscheck_CloudSigma() {
     # http://paste.ubuntu.com/23624795/
-    dmi_product_name_is "CloudSigma" && return $DS_FOUND
+    dmi_product_name_matches "CloudSigma" && return $DS_FOUND
     return $DS_NOT_FOUND
 }
 
@@ -654,6 +664,8 @@ dscheck_Azure() {
     #  UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209"
     #  TYPE="udf">/dev/sr0</device>
     #
+    local AZURE_CHASSIS="7783-7084-3265-9085-8269-3286-77"
+    dmi_chassis_asset_tag_matches $AZURE_CHASSIS && return $DS_FOUND
     check_seed_dir azure ovf-env.xml && return ${DS_FOUND}
 
     [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND}
@@ -786,7 +798,7 @@ dscheck_Ec2() {
 }
 
 dscheck_GCE() {
-    if dmi_product_name_is "Google Compute Engine"; then
+    if dmi_product_name_matches "Google Compute Engine"; then
         return ${DS_FOUND}
     fi
     # product name is not guaranteed (LP: #1674861)
@@ -807,10 +819,10 @@ dscheck_OpenStack() {
         return ${DS_NOT_FOUND}
     fi
     local nova="OpenStack Nova" compute="OpenStack Compute"
-    if dmi_product_name_is "$nova"; then
+    if dmi_product_name_matches "$nova"; then
         return ${DS_FOUND}
     fi
-    if dmi_product_name_is "$compute"; then
+    if dmi_product_name_matches "$compute"; then
         # RDO installed nova (LP: #1675349).
         return ${DS_FOUND}
     fi
@@ -889,6 +901,7 @@ collect_info() {
     read_config
     read_datasource_list
     read_dmi_sys_vendor
+    read_dmi_chassis_asset_tag
     read_dmi_product_name
     read_dmi_product_serial
     read_dmi_product_uuid
_______________________________________________
Mailing list: https://launchpad.net/~cloud-init-dev
Post to     : cloud-init-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~cloud-init-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to