Merge lp:~oddbloke/cloud-init/lp1506187 into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Dan Watkins
Status: Merged
Merged at revision: 1156
Proposed branch: lp:~oddbloke/cloud-init/lp1506187
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 379 lines (+23/-155)
4 files modified
cloudinit/sources/DataSourceAzure.py (+3/-35)
cloudinit/sources/helpers/azure.py (+0/-21)
tests/unittests/test_datasource/test_azure.py (+18/-59)
tests/unittests/test_datasource/test_azure_helper.py (+2/-40)
To merge this branch: bzr merge lp:~oddbloke/cloud-init/lp1506187
Reviewer Review Type Date Requested Status
Ben Howard (community) Approve
Scott Moser Pending
Review via email: mp+276292@code.launchpad.net

Commit message

Azure: Use DMI data to find instance IDs.

Replace the use of SharedConfig.xml in both the walinuxagent case,
and the case where we communicate with the Azure fabric ourselves.

To post a comment you must log in.
Revision history for this message
Ben Howard (darkmuggle-deactivatedaccount) wrote :

Adding Scott to the reviewers for merge approval and merging.

This change looks sane to me and reduces the complexity of the Datasource. Microsoft has updated the public facing documentation to reflect the use of the system uuid as extracted from dmi data as the authoratative identification of an instance.

We are seeing this as a hot issue. So once this lands in Xenial, I'll run an SRU to get this backported.

+1

review: Approve
Revision history for this message
Scott Moser (smoser) wrote :

This looks good. The one question I have is whether or not a instance that used sharedconfig.xml last boot will now think it is a new instance after an upgrade of cloud-init and reboot.

specifically, is the instance id in DMI the same as the instance id in SharedConfig.xml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceAzure.py'
2--- cloudinit/sources/DataSourceAzure.py 2015-10-15 00:27:07 +0000
3+++ cloudinit/sources/DataSourceAzure.py 2015-10-30 16:36:54 +0000
4@@ -31,8 +31,7 @@
5 from cloudinit.settings import PER_ALWAYS
6 from cloudinit import sources
7 from cloudinit import util
8-from cloudinit.sources.helpers.azure import (
9- get_metadata_from_fabric, iid_from_shared_config_content)
10+from cloudinit.sources.helpers.azure import get_metadata_from_fabric
11
12 LOG = logging.getLogger(__name__)
13
14@@ -41,7 +40,6 @@
15 AGENT_START = ['service', 'walinuxagent', 'start']
16 BOUNCE_COMMAND = ['sh', '-xc',
17 "i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x"]
18-DATA_DIR_CLEAN_LIST = ['SharedConfig.xml']
19
20 BUILTIN_DS_CONFIG = {
21 'agent_command': AGENT_START,
22@@ -144,8 +142,6 @@
23 self.ds_cfg['agent_command'])
24
25 ddir = self.ds_cfg['data_dir']
26- shcfgxml = os.path.join(ddir, "SharedConfig.xml")
27- wait_for = [shcfgxml]
28
29 fp_files = []
30 key_value = None
31@@ -160,19 +156,11 @@
32
33 missing = util.log_time(logfunc=LOG.debug, msg="waiting for files",
34 func=wait_for_files,
35- args=(wait_for + fp_files,))
36+ args=(fp_files,))
37 if len(missing):
38 LOG.warn("Did not find files, but going on: %s", missing)
39
40 metadata = {}
41- if shcfgxml in missing:
42- LOG.warn("SharedConfig.xml missing, using static instance-id")
43- else:
44- try:
45- metadata['instance-id'] = iid_from_shared_config(shcfgxml)
46- except ValueError as e:
47- LOG.warn("failed to get instance id in %s: %s", shcfgxml, e)
48-
49 metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)
50 return metadata
51
52@@ -229,21 +217,6 @@
53 user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
54 self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
55
56- if found != ddir:
57- cached_ovfenv = util.load_file(
58- os.path.join(ddir, 'ovf-env.xml'), quiet=True, decode=False)
59- if cached_ovfenv != files['ovf-env.xml']:
60- # source was not walinux-agent's datadir, so we have to clean
61- # up so 'wait_for_files' doesn't return early due to stale data
62- cleaned = []
63- for f in [os.path.join(ddir, f) for f in DATA_DIR_CLEAN_LIST]:
64- if os.path.exists(f):
65- util.del_file(f)
66- cleaned.append(f)
67- if cleaned:
68- LOG.info("removed stale file(s) in '%s': %s",
69- ddir, str(cleaned))
70-
71 # walinux agent writes files world readable, but expects
72 # the directory to be protected.
73 write_files(ddir, files, dirmode=0o700)
74@@ -259,6 +232,7 @@
75 " on Azure.", exc_info=True)
76 return False
77
78+ self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
79 self.metadata.update(fabric_data)
80
81 found_ephemeral = find_fabric_formatted_ephemeral_disk()
82@@ -649,12 +623,6 @@
83 return (md, ud, cfg, {'ovf-env.xml': contents})
84
85
86-def iid_from_shared_config(path):
87- with open(path, "rb") as fp:
88- content = fp.read()
89- return iid_from_shared_config_content(content)
90-
91-
92 class BrokenAzureDataSource(Exception):
93 pass
94
95
96=== modified file 'cloudinit/sources/helpers/azure.py'
97--- cloudinit/sources/helpers/azure.py 2015-05-08 15:52:58 +0000
98+++ cloudinit/sources/helpers/azure.py 2015-10-30 16:36:54 +0000
99@@ -79,12 +79,6 @@
100 './Container/RoleInstanceList/RoleInstance/InstanceId')
101
102 @property
103- def shared_config_xml(self):
104- url = self._text_from_xpath('./Container/RoleInstanceList/RoleInstance'
105- '/Configuration/SharedConfig')
106- return self.http_client.get(url).contents
107-
108- @property
109 def certificates_xml(self):
110 if self._certificates_xml is None:
111 url = self._text_from_xpath(
112@@ -172,19 +166,6 @@
113 return keys
114
115
116-def iid_from_shared_config_content(content):
117- """
118- find INSTANCE_ID in:
119- <?xml version="1.0" encoding="utf-8"?>
120- <SharedConfig version="1.0.0.0" goalStateIncarnation="1">
121- <Deployment name="INSTANCE_ID" guid="{...}" incarnation="0">
122- <Service name="..." guid="{00000000-0000-0000-0000-000000000000}"/>
123- """
124- root = ElementTree.fromstring(content)
125- depnode = root.find('Deployment')
126- return depnode.get('name')
127-
128-
129 class WALinuxAgentShim(object):
130
131 REPORT_READY_XML_TEMPLATE = '\n'.join([
132@@ -263,8 +244,6 @@
133 public_keys = self.openssl_manager.parse_certificates(
134 goal_state.certificates_xml)
135 data = {
136- 'instance-id': iid_from_shared_config_content(
137- goal_state.shared_config_xml),
138 'public-keys': public_keys,
139 }
140 self._report_ready(goal_state, http_client)
141
142=== modified file 'tests/unittests/test_datasource/test_azure.py'
143--- tests/unittests/test_datasource/test_azure.py 2015-10-14 23:32:35 +0000
144+++ tests/unittests/test_datasource/test_azure.py 2015-10-30 16:36:54 +0000
145@@ -115,10 +115,6 @@
146 data['pubkey_files'] = flist
147 return ["pubkey_from: %s" % f for f in flist]
148
149- def _iid_from_shared_config(path):
150- data['iid_from_shared_cfg'] = path
151- return 'i-my-azure-id'
152-
153 if data.get('ovfcontent') is not None:
154 populate_dir(os.path.join(self.paths.seed_dir, "azure"),
155 {'ovf-env.xml': data['ovfcontent']})
156@@ -127,20 +123,22 @@
157 mod.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
158
159 self.get_metadata_from_fabric = mock.MagicMock(return_value={
160- 'instance-id': 'i-my-azure-id',
161 'public-keys': [],
162 })
163
164+ self.instance_id = 'test-instance-id'
165+
166 self.apply_patches([
167 (mod, 'list_possible_azure_ds_devs', dsdevs),
168 (mod, 'invoke_agent', _invoke_agent),
169 (mod, 'wait_for_files', _wait_for_files),
170 (mod, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),
171- (mod, 'iid_from_shared_config', _iid_from_shared_config),
172 (mod, 'perform_hostname_bounce', mock.MagicMock()),
173 (mod, 'get_hostname', mock.MagicMock()),
174 (mod, 'set_hostname', mock.MagicMock()),
175 (mod, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
176+ (mod.util, 'read_dmi_data', mock.MagicMock(
177+ return_value=self.instance_id)),
178 ])
179
180 dsrc = mod.DataSourceAzureNet(
181@@ -193,7 +191,6 @@
182 self.assertEqual(dsrc.metadata['local-hostname'], odata['HostName'])
183 self.assertTrue(os.path.isfile(
184 os.path.join(self.waagent_d, 'ovf-env.xml')))
185- self.assertEqual(dsrc.metadata['instance-id'], 'i-my-azure-id')
186
187 def test_waagent_d_has_0700_perms(self):
188 # we expect /var/lib/waagent to be created 0700
189@@ -345,7 +342,6 @@
190 for mypk in mypklist:
191 self.assertIn(mypk['value'], dsrc.metadata['public-keys'])
192
193-
194 def test_default_ephemeral(self):
195 # make sure the ephemeral device works
196 odata = {}
197@@ -434,54 +430,6 @@
198 dsrc = self._get_ds({'ovfcontent': xml})
199 dsrc.get_data()
200
201- def test_existing_ovf_same(self):
202- # waagent/SharedConfig left alone if found ovf-env.xml same as cached
203- odata = {'UserData': b64e("SOMEUSERDATA")}
204- data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
205-
206- populate_dir(self.waagent_d,
207- {'ovf-env.xml': data['ovfcontent'],
208- 'otherfile': 'otherfile-content',
209- 'SharedConfig.xml': 'mysharedconfig'})
210-
211- dsrc = self._get_ds(data)
212- ret = dsrc.get_data()
213- self.assertTrue(ret)
214- self.assertTrue(os.path.exists(
215- os.path.join(self.waagent_d, 'ovf-env.xml')))
216- self.assertTrue(os.path.exists(
217- os.path.join(self.waagent_d, 'otherfile')))
218- self.assertTrue(os.path.exists(
219- os.path.join(self.waagent_d, 'SharedConfig.xml')))
220-
221- def test_existing_ovf_diff(self):
222- # waagent/SharedConfig must be removed if ovfenv is found elsewhere
223-
224- # 'get_data' should remove SharedConfig.xml in /var/lib/waagent
225- # if ovf-env.xml differs.
226- cached_ovfenv = construct_valid_ovf_env(
227- {'userdata': b64e("FOO_USERDATA")})
228- new_ovfenv = construct_valid_ovf_env(
229- {'userdata': b64e("NEW_USERDATA")})
230-
231- populate_dir(self.waagent_d,
232- {'ovf-env.xml': cached_ovfenv,
233- 'SharedConfig.xml': "mysharedconfigxml",
234- 'otherfile': 'otherfilecontent'})
235-
236- dsrc = self._get_ds({'ovfcontent': new_ovfenv})
237- ret = dsrc.get_data()
238- self.assertTrue(ret)
239- self.assertEqual(dsrc.userdata_raw, b"NEW_USERDATA")
240- self.assertTrue(os.path.exists(
241- os.path.join(self.waagent_d, 'otherfile')))
242- self.assertFalse(os.path.exists(
243- os.path.join(self.waagent_d, 'SharedConfig.xml')))
244- self.assertTrue(os.path.exists(
245- os.path.join(self.waagent_d, 'ovf-env.xml')))
246- new_xml = load_file(os.path.join(self.waagent_d, 'ovf-env.xml'))
247- self.xml_equals(new_ovfenv, new_xml)
248-
249 def test_exception_fetching_fabric_data_doesnt_propagate(self):
250 ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
251 ds.ds_cfg['agent_command'] = '__builtin__'
252@@ -496,6 +444,17 @@
253 self.assertTrue(ret)
254 self.assertEqual('value', ds.metadata['test'])
255
256+ def test_instance_id_from_dmidecode_used(self):
257+ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
258+ ds.get_data()
259+ self.assertEqual(self.instance_id, ds.metadata['instance-id'])
260+
261+ def test_instance_id_from_dmidecode_used_for_builtin(self):
262+ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
263+ ds.ds_cfg['agent_command'] = '__builtin__'
264+ ds.get_data()
265+ self.assertEqual(self.instance_id, ds.metadata['instance-id'])
266+
267
268 class TestAzureBounce(TestCase):
269
270@@ -505,9 +464,6 @@
271 self.patches.enter_context(
272 mock.patch.object(DataSourceAzure, 'wait_for_files'))
273 self.patches.enter_context(
274- mock.patch.object(DataSourceAzure, 'iid_from_shared_config',
275- mock.MagicMock(return_value='i-my-azure-id')))
276- self.patches.enter_context(
277 mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',
278 mock.MagicMock(return_value=[])))
279 self.patches.enter_context(
280@@ -521,6 +477,9 @@
281 self.patches.enter_context(
282 mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',
283 mock.MagicMock(return_value={})))
284+ self.patches.enter_context(
285+ mock.patch.object(DataSourceAzure.util, 'read_dmi_data',
286+ mock.MagicMock(return_value='test-instance-id')))
287
288 def setUp(self):
289 super(TestAzureBounce, self).setUp()
290
291=== modified file 'tests/unittests/test_datasource/test_azure_helper.py'
292--- tests/unittests/test_datasource/test_azure_helper.py 2015-05-15 20:28:24 +0000
293+++ tests/unittests/test_datasource/test_azure_helper.py 2015-10-30 16:36:54 +0000
294@@ -40,7 +40,7 @@
295 <HostingEnvironmentConfig>
296 http://100.86.192.70:80/...hostingEnvironmentConfig...
297 </HostingEnvironmentConfig>
298- <SharedConfig>{shared_config_url}</SharedConfig>
299+ <SharedConfig>http://100.86.192.70:80/..SharedConfig..</SharedConfig>
300 <ExtensionsConfig>
301 http://100.86.192.70:80/...extensionsConfig...
302 </ExtensionsConfig>
303@@ -55,21 +55,6 @@
304 """
305
306
307-class TestReadAzureSharedConfig(unittest.TestCase):
308-
309- def test_valid_content(self):
310- xml = """<?xml version="1.0" encoding="utf-8"?>
311- <SharedConfig>
312- <Deployment name="MY_INSTANCE_ID">
313- <Service name="myservice"/>
314- <ServiceInstance name="INSTANCE_ID.0" guid="{abcd-uuid}" />
315- </Deployment>
316- <Incarnation number="1"/>
317- </SharedConfig>"""
318- ret = azure_helper.iid_from_shared_config_content(xml)
319- self.assertEqual("MY_INSTANCE_ID", ret)
320-
321-
322 class TestFindEndpoint(TestCase):
323
324 def setUp(self):
325@@ -140,7 +125,6 @@
326 'incarnation': 1,
327 'container_id': 'MyContainerId',
328 'instance_id': 'MyInstanceId',
329- 'shared_config_url': 'MySharedConfigUrl',
330 'certificates_url': 'MyCertificatesUrl',
331 }
332
333@@ -174,20 +158,9 @@
334 goal_state = self._get_goal_state(instance_id=instance_id)
335 self.assertEqual(instance_id, goal_state.instance_id)
336
337- def test_shared_config_xml_parsed_and_fetched_correctly(self):
338- http_client = mock.MagicMock()
339- shared_config_url = 'TestSharedConfigUrl'
340- goal_state = self._get_goal_state(
341- http_client=http_client, shared_config_url=shared_config_url)
342- shared_config_xml = goal_state.shared_config_xml
343- self.assertEqual(1, http_client.get.call_count)
344- self.assertEqual(shared_config_url, http_client.get.call_args[0][0])
345- self.assertEqual(http_client.get.return_value.contents,
346- shared_config_xml)
347-
348 def test_certificates_xml_parsed_and_fetched_correctly(self):
349 http_client = mock.MagicMock()
350- certificates_url = 'TestSharedConfigUrl'
351+ certificates_url = 'TestCertificatesUrl'
352 goal_state = self._get_goal_state(
353 http_client=http_client, certificates_url=certificates_url)
354 certificates_xml = goal_state.certificates_xml
355@@ -324,8 +297,6 @@
356 azure_helper.WALinuxAgentShim, 'find_endpoint'))
357 self.GoalState = patches.enter_context(
358 mock.patch.object(azure_helper, 'GoalState'))
359- self.iid_from_shared_config_content = patches.enter_context(
360- mock.patch.object(azure_helper, 'iid_from_shared_config_content'))
361 self.OpenSSLManager = patches.enter_context(
362 mock.patch.object(azure_helper, 'OpenSSLManager'))
363 patches.enter_context(
364@@ -367,15 +338,6 @@
365 data = shim.register_with_azure_and_fetch_data()
366 self.assertEqual([], data['public-keys'])
367
368- def test_instance_id_returned_in_data(self):
369- shim = azure_helper.WALinuxAgentShim()
370- data = shim.register_with_azure_and_fetch_data()
371- self.assertEqual(
372- [mock.call(self.GoalState.return_value.shared_config_xml)],
373- self.iid_from_shared_config_content.call_args_list)
374- self.assertEqual(self.iid_from_shared_config_content.return_value,
375- data['instance-id'])
376-
377 def test_correct_url_used_for_report_ready(self):
378 self.find_endpoint.return_value = 'test_endpoint'
379 shim = azure_helper.WALinuxAgentShim()