Merge ~chad.smith/cloud-init:azure-di-id-asset-tag into cloud-init:master

Proposed by Chad Smith on 2017-05-31
Status: Merged
Merged at revision: 5fb49bacf7441d8d20a7b4e0e7008ca586f5ebab
Proposed branch: ~chad.smith/cloud-init:azure-di-id-asset-tag
Merge into: cloud-init:master
Diff against target: 332 lines (+133/-15)
4 files modified
cloudinit/sources/DataSourceAzure.py (+7/-1)
tests/unittests/test_datasource/test_azure.py (+63/-3)
tests/unittests/test_ds_identify.py (+39/-0)
tools/ds-identify (+24/-11)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-06-01
Scott Moser 2017-05-31 Approve on 2017-06-01
Review via email: mp+324875@code.launchpad.net

Description of the Change

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

To post a comment you must log in.
Scott Moser (smoser) wrote :

https://gist.github.com/smoser/a5435f49b61e2ae45c367bbb266bfac3

Add DI_DMI_CHASSIS_ASSET_TAG to _print_info in tools/ds-identify.

fc29533... by Chad Smith on 2017-06-01

dmi_product_name_is is dropped from ds-identify, fix it for AliYun datasource. unit test mock fixes for read_dmi_data

Chad Smith (chad.smith) :
4ce9ca1... by Chad Smith on 2017-06-01

resolve merge conflicts

d2abad2... by Chad Smith on 2017-06-01

add unit test for variables printed by ds-identify's _print_info

2275666... by Chad Smith on 2017-06-01

lint and drop custom NonAzureDataSource message

Scott Moser (smoser) wrote :

I approve with some minor changes.
http://paste.ubuntu.com/24738693/

if you want to ACK those i can just integrate themm, squash and merge.

review: Approve
54480ea... by Chad Smith on 2017-06-01

    smoser fixups

     - fix up comment spelling
     - drop unused change.
     - capital letters are for global vars

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
2index b9458ff..a0b9eae 100644
3--- a/cloudinit/sources/DataSourceAzure.py
4+++ b/cloudinit/sources/DataSourceAzure.py
5@@ -36,6 +36,8 @@ RESOURCE_DISK_PATH = '/dev/disk/cloud/azure_resource'
6 DEFAULT_PRIMARY_NIC = 'eth0'
7 LEASE_FILE = '/var/lib/dhcp/dhclient.eth0.leases'
8 DEFAULT_FS = 'ext4'
9+# DMI chassis-asset-tag is set static for all azure instances
10+AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
11
12
13 def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):
14@@ -320,6 +322,10 @@ class DataSourceAzureNet(sources.DataSource):
15 # azure removes/ejects the cdrom containing the ovf-env.xml
16 # file on reboot. So, in order to successfully reboot we
17 # need to look in the datadir and consider that valid
18+ asset_tag = util.read_dmi_data('chassis-asset-tag')
19+ if asset_tag != AZURE_CHASSIS_ASSET_TAG:
20+ LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
21+ return False
22 ddir = self.ds_cfg['data_dir']
23
24 candidates = [self.seed_dir]
25@@ -694,7 +700,7 @@ def read_azure_ovf(contents):
26 try:
27 dom = minidom.parseString(contents)
28 except Exception as e:
29- raise BrokenAzureDataSource("invalid xml: %s" % e)
30+ raise BrokenAzureDataSource("Invalid ovf-env.xml: %s" % e)
31
32 results = find_child(dom.documentElement,
33 lambda n: n.localName == "ProvisioningSection")
34diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
35index 852ec70..42f49e0 100644
36--- a/tests/unittests/test_datasource/test_azure.py
37+++ b/tests/unittests/test_datasource/test_azure.py
38@@ -76,7 +76,9 @@ def construct_valid_ovf_env(data=None, pubkeys=None, userdata=None):
39 return content
40
41
42-class TestAzureDataSource(TestCase):
43+class TestAzureDataSource(CiTestCase):
44+
45+ with_logs = True
46
47 def setUp(self):
48 super(TestAzureDataSource, self).setUp()
49@@ -160,6 +162,12 @@ scbus-1 on xpt0 bus 0
50
51 self.instance_id = 'test-instance-id'
52
53+ def _dmi_mocks(key):
54+ if key == 'system-uuid':
55+ return self.instance_id
56+ elif key == 'chassis-asset-tag':
57+ return '7783-7084-3265-9085-8269-3286-77'
58+
59 self.apply_patches([
60 (dsaz, 'list_possible_azure_ds_devs', dsdevs),
61 (dsaz, 'invoke_agent', _invoke_agent),
62@@ -170,7 +178,7 @@ scbus-1 on xpt0 bus 0
63 (dsaz, 'set_hostname', mock.MagicMock()),
64 (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
65 (dsaz.util, 'read_dmi_data', mock.MagicMock(
66- return_value=self.instance_id)),
67+ side_effect=_dmi_mocks)),
68 ])
69
70 dsrc = dsaz.DataSourceAzureNet(
71@@ -241,6 +249,23 @@ fdescfs /dev/fd fdescfs rw 0 0
72 res = get_path_dev_freebsd('/etc', mnt_list)
73 self.assertIsNotNone(res)
74
75+ @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')
76+ def test_non_azure_dmi_chassis_asset_tag(self, m_read_dmi_data):
77+ """Report non-azure when DMI's chassis asset tag doesn't match.
78+
79+ Return False when the asset tag doesn't match Azure's static
80+ AZURE_CHASSIS_ASSET_TAG.
81+ """
82+ # Return a non-matching asset tag value
83+ nonazure_tag = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'
84+ m_read_dmi_data.return_value = nonazure_tag
85+ dsrc = dsaz.DataSourceAzureNet(
86+ {}, distro=None, paths=self.paths)
87+ self.assertFalse(dsrc.get_data())
88+ self.assertEqual(
89+ "Non-Azure DMI asset tag '{0}' discovered.\n".format(nonazure_tag),
90+ self.logs.getvalue())
91+
92 def test_basic_seed_dir(self):
93 odata = {'HostName': "myhost", 'UserName': "myuser"}
94 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
95@@ -531,9 +556,17 @@ class TestAzureBounce(TestCase):
96 self.patches.enter_context(
97 mock.patch.object(dsaz, 'get_metadata_from_fabric',
98 mock.MagicMock(return_value={})))
99+
100+ def _dmi_mocks(key):
101+ if key == 'system-uuid':
102+ return 'test-instance-id'
103+ elif key == 'chassis-asset-tag':
104+ return '7783-7084-3265-9085-8269-3286-77'
105+ raise RuntimeError('should not get here')
106+
107 self.patches.enter_context(
108 mock.patch.object(dsaz.util, 'read_dmi_data',
109- mock.MagicMock(return_value='test-instance-id')))
110+ mock.MagicMock(side_effect=_dmi_mocks)))
111
112 def setUp(self):
113 super(TestAzureBounce, self).setUp()
114@@ -696,6 +729,33 @@ class TestAzureBounce(TestCase):
115 self.assertEqual(0, self.set_hostname.call_count)
116
117
118+class TestLoadAzureDsDir(CiTestCase):
119+ """Tests for load_azure_ds_dir."""
120+
121+ def setUp(self):
122+ self.source_dir = self.tmp_dir()
123+ super(TestLoadAzureDsDir, self).setUp()
124+
125+ def test_missing_ovf_env_xml_raises_non_azure_datasource_error(self):
126+ """load_azure_ds_dir raises an error When ovf-env.xml doesn't exit."""
127+ with self.assertRaises(dsaz.NonAzureDataSource) as context_manager:
128+ dsaz.load_azure_ds_dir(self.source_dir)
129+ self.assertEqual(
130+ 'No ovf-env file found',
131+ str(context_manager.exception))
132+
133+ def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self):
134+ """load_azure_ds_dir calls read_azure_ovf to parse the xml."""
135+ ovf_path = os.path.join(self.source_dir, 'ovf-env.xml')
136+ with open(ovf_path, 'wb') as stream:
137+ stream.write(b'invalid xml')
138+ with self.assertRaises(dsaz.BrokenAzureDataSource) as context_manager:
139+ dsaz.load_azure_ds_dir(self.source_dir)
140+ self.assertEqual(
141+ 'Invalid ovf-env.xml: syntax error: line 1, column 0',
142+ str(context_manager.exception))
143+
144+
145 class TestReadAzureOvf(TestCase):
146 def test_invalid_xml_raises_non_azure_ds(self):
147 invalid_xml = "<foo>" + construct_valid_ovf_env(data={})
148diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
149index 5c26e65..8ccfe55 100644
150--- a/tests/unittests/test_ds_identify.py
151+++ b/tests/unittests/test_ds_identify.py
152@@ -39,9 +39,11 @@ RC_FOUND = 0
153 RC_NOT_FOUND = 1
154 DS_NONE = 'None'
155
156+P_CHASSIS_ASSET_TAG = "sys/class/dmi/id/chassis_asset_tag"
157 P_PRODUCT_NAME = "sys/class/dmi/id/product_name"
158 P_PRODUCT_SERIAL = "sys/class/dmi/id/product_serial"
159 P_PRODUCT_UUID = "sys/class/dmi/id/product_uuid"
160+P_SEED_DIR = "var/lib/cloud/seed"
161 P_DSID_CFG = "etc/cloud/ds-identify.cfg"
162
163 MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}
164@@ -160,6 +162,30 @@ class TestDsIdentify(CiTestCase):
165 _print_run_output(rc, out, err, cfg, files)
166 return rc, out, err, cfg, files
167
168+ def test_wb_print_variables(self):
169+ """_print_info reports an array of discovered variables to stderr."""
170+ data = VALID_CFG['Azure-dmi-detection']
171+ _, _, err, _, _ = self._call_via_dict(data)
172+ expected_vars = [
173+ 'DMI_PRODUCT_NAME', 'DMI_SYS_VENDOR', 'DMI_PRODUCT_SERIAL',
174+ 'DMI_PRODUCT_UUID', 'PID_1_PRODUCT_NAME', 'DMI_CHASSIS_ASSET_TAG',
175+ 'FS_LABELS', 'KERNEL_CMDLINE', 'VIRT', 'UNAME_KERNEL_NAME',
176+ 'UNAME_KERNEL_RELEASE', 'UNAME_KERNEL_VERSION', 'UNAME_MACHINE',
177+ 'UNAME_NODENAME', 'UNAME_OPERATING_SYSTEM', 'DSNAME', 'DSLIST',
178+ 'MODE', 'ON_FOUND', 'ON_MAYBE', 'ON_NOTFOUND']
179+ for var in expected_vars:
180+ self.assertIn('{0}='.format(var), err)
181+
182+ def test_azure_dmi_detection_from_chassis_asset_tag(self):
183+ """Azure datasource is detected from DMI chassis-asset-tag"""
184+ self._test_ds_found('Azure-dmi-detection')
185+
186+ def test_azure_seed_file_detection(self):
187+ """Azure datasource is detected due to presence of a seed file.
188+
189+ The seed file tested is /var/lib/cloud/seed/azure/ovf-env.xml."""
190+ self._test_ds_found('Azure-seed-detection')
191+
192 def test_aws_ec2_hvm(self):
193 """EC2: hvm instances use dmi serial and uuid starting with 'ec2'."""
194 self._test_ds_found('Ec2-hvm')
195@@ -272,6 +298,19 @@ VALID_CFG = {
196 'ds': 'AliYun',
197 'files': {P_PRODUCT_NAME: 'Alibaba Cloud ECS\n'},
198 },
199+ 'Azure-dmi-detection': {
200+ 'ds': 'Azure',
201+ 'files': {
202+ P_CHASSIS_ASSET_TAG: '7783-7084-3265-9085-8269-3286-77\n',
203+ }
204+ },
205+ 'Azure-seed-detection': {
206+ 'ds': 'Azure',
207+ 'files': {
208+ P_CHASSIS_ASSET_TAG: 'No-match\n',
209+ os.path.join(P_SEED_DIR, 'azure', 'ovf-env.xml'): 'present\n',
210+ }
211+ },
212 'Ec2-hvm': {
213 'ds': 'Ec2',
214 'mocks': [{'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}],
215diff --git a/tools/ds-identify b/tools/ds-identify
216index 5fc500b..546e0f5 100755
217--- a/tools/ds-identify
218+++ b/tools/ds-identify
219@@ -85,6 +85,7 @@ DI_MAIN=${DI_MAIN:-main}
220
221 DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
222 DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
223+DI_DMI_CHASSIS_ASSET_TAG=""
224 DI_DMI_PRODUCT_NAME=""
225 DI_DMI_SYS_VENDOR=""
226 DI_DMI_PRODUCT_SERIAL=""
227@@ -259,6 +260,12 @@ read_kernel_cmdline() {
228 DI_KERNEL_CMDLINE="$cmdline"
229 }
230
231+read_dmi_chassis_asset_tag() {
232+ cached "${DI_DMI_CHASSIS_ASSET_TAG}" && return
233+ get_dmi_field chassis_asset_tag
234+ DI_DMI_CHASSIS_ASSET_TAG="$_RET"
235+}
236+
237 read_dmi_sys_vendor() {
238 cached "${DI_DMI_SYS_VENDOR}" && return
239 get_dmi_field sys_vendor
240@@ -386,6 +393,14 @@ read_pid1_product_name() {
241 DI_PID_1_PRODUCT_NAME="$product_name"
242 }
243
244+dmi_chassis_asset_tag_matches() {
245+ is_container && return 1
246+ case "${DI_DMI_CHASSIS_ASSET_TAG}" in
247+ $1) return 0;;
248+ esac
249+ return 1
250+}
251+
252 dmi_product_name_matches() {
253 is_container && return 1
254 case "${DI_DMI_PRODUCT_NAME}" in
255@@ -402,11 +417,6 @@ dmi_product_serial_matches() {
256 return 1
257 }
258
259-dmi_product_name_is() {
260- is_container && return 1
261- [ "${DI_DMI_PRODUCT_NAME}" = "$1" ]
262-}
263-
264 dmi_sys_vendor_is() {
265 is_container && return 1
266 [ "${DI_DMI_SYS_VENDOR}" = "$1" ]
267@@ -478,7 +488,7 @@ dscheck_CloudStack() {
268
269 dscheck_CloudSigma() {
270 # http://paste.ubuntu.com/23624795/
271- dmi_product_name_is "CloudSigma" && return $DS_FOUND
272+ dmi_product_name_matches "CloudSigma" && return $DS_FOUND
273 return $DS_NOT_FOUND
274 }
275
276@@ -654,6 +664,8 @@ dscheck_Azure() {
277 # UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209"
278 # TYPE="udf">/dev/sr0</device>
279 #
280+ local azure_chassis="7783-7084-3265-9085-8269-3286-77"
281+ dmi_chassis_asset_tag_matches "${azure_chassis}" && return $DS_FOUND
282 check_seed_dir azure ovf-env.xml && return ${DS_FOUND}
283
284 [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND}
285@@ -786,7 +798,7 @@ dscheck_Ec2() {
286 }
287
288 dscheck_GCE() {
289- if dmi_product_name_is "Google Compute Engine"; then
290+ if dmi_product_name_matches "Google Compute Engine"; then
291 return ${DS_FOUND}
292 fi
293 # product name is not guaranteed (LP: #1674861)
294@@ -807,10 +819,10 @@ dscheck_OpenStack() {
295 return ${DS_NOT_FOUND}
296 fi
297 local nova="OpenStack Nova" compute="OpenStack Compute"
298- if dmi_product_name_is "$nova"; then
299+ if dmi_product_name_matches "$nova"; then
300 return ${DS_FOUND}
301 fi
302- if dmi_product_name_is "$compute"; then
303+ if dmi_product_name_matches "$compute"; then
304 # RDO installed nova (LP: #1675349).
305 return ${DS_FOUND}
306 fi
307@@ -823,7 +835,7 @@ dscheck_OpenStack() {
308
309 dscheck_AliYun() {
310 check_seed_dir "AliYun" meta-data user-data && return ${DS_FOUND}
311- if dmi_product_name_is "Alibaba Cloud ECS"; then
312+ if dmi_product_name_matches "Alibaba Cloud ECS"; then
313 return $DS_FOUND
314 fi
315 return $DS_NOT_FOUND
316@@ -889,6 +901,7 @@ collect_info() {
317 read_config
318 read_datasource_list
319 read_dmi_sys_vendor
320+ read_dmi_chassis_asset_tag
321 read_dmi_product_name
322 read_dmi_product_serial
323 read_dmi_product_uuid
324@@ -903,7 +916,7 @@ print_info() {
325 _print_info() {
326 local n="" v="" vars=""
327 vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL"
328- vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME"
329+ vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME DMI_CHASSIS_ASSET_TAG"
330 vars="$vars FS_LABELS KERNEL_CMDLINE VIRT"
331 vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_RELEASE UNAME_KERNEL_VERSION"
332 vars="$vars UNAME_MACHINE UNAME_NODENAME UNAME_OPERATING_SYSTEM"

Subscribers

People subscribed via source and target branches