Merge ~smoser/cloud-init:ubuntu/xenial into cloud-init:ubuntu/xenial

Proposed by Scott Moser
Status: Merged
Merged at revision: 57d5278faeb37550275967b39b2573532fa20ca7
Proposed branch: ~smoser/cloud-init:ubuntu/xenial
Merge into: cloud-init:ubuntu/xenial
Diff against target: 2029 lines (+1979/-2)
7 files modified
debian/changelog (+12/-2)
debian/patches/cpick-003c6678-net-remove-systemd-link-file-writing-from-eni-renderer (+95/-0)
debian/patches/cpick-11121fe4-systemd-make-cloud-final.service-run-before-apt-daily (+33/-0)
debian/patches/cpick-1cd4323b-azure-remove-accidental-duplicate-line-in-merge (+22/-0)
debian/patches/cpick-5fb49bac-azure-identify-platform-by-well-known-value-in-chassis (+338/-0)
debian/patches/cpick-ebc9ecbc-Azure-Add-network-config-Refactor-net-layer-to-handle (+1474/-0)
debian/patches/series (+5/-0)
Reviewer Review Type Date Requested Status
Joshua Powers (community) Approve
Server Team CI bot continuous-integration Needs Fixing
Ryan Harper Approve
Review via email: mp+326452@code.launchpad.net

Commit message

place holder

Description of the change

This just cherry picks the following back to xenial for sru upload

5fb49bac: azure: identify platform by well known value in chassis asset (LP: #1693939)
  https://git.launchpad.net/cloud-init/commit/?id=5fb49bac

003c6678: net: remove systemd link file writing from eni renderer
  https://git.launchpad.net/cloud-init/commit/?id=003c6678

1cd4323b: azure: remove accidental duplicate line in merge.
  https://git.launchpad.net/cloud-init/commit/?id=1cd4323b

ebc9ecbc: Azure: Add network-config, Refactor net layer to handle
  https://git.launchpad.net/cloud-init/commit/?id=ebc9ecbc

11121fe4: systemd: make cloud-final.service run before apt daily services.
  https://git.launchpad.net/cloud-init/commit/?id=11121fe4

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:64db24de0983677bea9100055a9b69384e3bd421
https://jenkins.ubuntu.com/server/job/cloud-init-ci/19/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/19/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Looks good.

review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:57d5278faeb37550275967b39b2573532fa20ca7
https://jenkins.ubuntu.com/server/job/cloud-init-ci/20/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/20/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/21/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/21/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Joshua Powers (powersj) wrote :

Ran the built deb using the latest tests from master and it passed:
https://paste.ubuntu.com/24975100/

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index caf1754..987f026 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,8 +1,18 @@
6 cloud-init (0.7.9-153-g16a7302f-0ubuntu1~16.04.2) UNRELEASED; urgency=medium
7
8 * debian/patches/ds-identify-behavior-xenial.patch: refresh patch.
9-
10- -- Scott Moser <smoser@ubuntu.com> Fri, 02 Jun 2017 16:19:26 -0400
11+ * cherry-pick 5fb49bac: azure: identify platform by well known value
12+ in chassis asset (LP: #1693939)
13+ * cherry-pick 003c6678: net: remove systemd link file writing from eni
14+ renderer
15+ * cherry-pick 1cd4323b: azure: remove accidental duplicate line in
16+ merge.
17+ * cherry-pick ebc9ecbc: Azure: Add network-config, Refactor net layer
18+ to handle
19+ * cherry-pick 11121fe4: systemd: make cloud-final.service run before
20+ apt daily (LP: #1693361)
21+
22+ -- Scott Moser <smoser@ubuntu.com> Wed, 28 Jun 2017 13:54:39 -0400
23
24 cloud-init (0.7.9-153-g16a7302f-0ubuntu1~16.04.1) xenial-proposed; urgency=medium
25
26diff --git a/debian/patches/cpick-003c6678-net-remove-systemd-link-file-writing-from-eni-renderer b/debian/patches/cpick-003c6678-net-remove-systemd-link-file-writing-from-eni-renderer
27new file mode 100644
28index 0000000..76504cc
29--- /dev/null
30+++ b/debian/patches/cpick-003c6678-net-remove-systemd-link-file-writing-from-eni-renderer
31@@ -0,0 +1,95 @@
32+From 003c6678e9c873b3b787a814016872b6592f5069 Mon Sep 17 00:00:00 2001
33+From: Ryan Harper <ryan.harper@canonical.com>
34+Date: Thu, 25 May 2017 15:37:15 -0500
35+Subject: [PATCH] net: remove systemd link file writing from eni renderer
36+
37+During the network v2 merge, we inadvertently re-enabled rendering systemd
38+.link files. This files are not required as cloud-init already has to do
39+interface renaming due to issues with udevd which may refuse to rename
40+certain interfaces (such as veth devices in a LXD container). As such,
41+removing the code altogether.
42+---
43+ cloudinit/net/eni.py | 25 -------------------------
44+ tests/unittests/test_net.py | 9 +++------
45+ 2 files changed, 3 insertions(+), 31 deletions(-)
46+
47+--- a/cloudinit/net/eni.py
48++++ b/cloudinit/net/eni.py
49+@@ -304,8 +304,6 @@ class Renderer(renderer.Renderer):
50+ config = {}
51+ self.eni_path = config.get('eni_path', 'etc/network/interfaces')
52+ self.eni_header = config.get('eni_header', None)
53+- self.links_path_prefix = config.get(
54+- 'links_path_prefix', 'etc/systemd/network/50-cloud-init-')
55+ self.netrules_path = config.get(
56+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
57+
58+@@ -451,28 +449,6 @@ class Renderer(renderer.Renderer):
59+ util.write_file(netrules,
60+ self._render_persistent_net(network_state))
61+
62+- if self.links_path_prefix:
63+- self._render_systemd_links(target, network_state,
64+- links_prefix=self.links_path_prefix)
65+-
66+- def _render_systemd_links(self, target, network_state, links_prefix):
67+- fp_prefix = util.target_path(target, links_prefix)
68+- for f in glob.glob(fp_prefix + "*"):
69+- os.unlink(f)
70+- for iface in network_state.iter_interfaces():
71+- if (iface['type'] == 'physical' and 'name' in iface and
72+- iface.get('mac_address')):
73+- fname = fp_prefix + iface['name'] + ".link"
74+- content = "\n".join([
75+- "[Match]",
76+- "MACAddress=" + iface['mac_address'],
77+- "",
78+- "[Link]",
79+- "Name=" + iface['name'],
80+- ""
81+- ])
82+- util.write_file(fname, content)
83+-
84+
85+ def network_state_to_eni(network_state, header=None, render_hwaddress=False):
86+ # render the provided network state, return a string of equivalent eni
87+@@ -480,7 +456,6 @@ def network_state_to_eni(network_state,
88+ renderer = Renderer(config={
89+ 'eni_path': eni_path,
90+ 'eni_header': header,
91+- 'links_path_prefix': None,
92+ 'netrules_path': None,
93+ })
94+ if not header:
95+--- a/tests/unittests/test_net.py
96++++ b/tests/unittests/test_net.py
97+@@ -992,9 +992,7 @@ class TestEniNetRendering(CiTestCase):
98+ os.makedirs(render_dir)
99+
100+ renderer = eni.Renderer(
101+- {'links_path_prefix': None,
102+- 'eni_path': 'interfaces', 'netrules_path': None,
103+- })
104++ {'eni_path': 'interfaces', 'netrules_path': None})
105+ renderer.render_network_state(ns, render_dir)
106+
107+ self.assertTrue(os.path.exists(os.path.join(render_dir,
108+@@ -1376,7 +1374,7 @@ class TestNetplanRoundTrip(CiTestCase):
109+
110+ class TestEniRoundTrip(CiTestCase):
111+ def _render_and_read(self, network_config=None, state=None, eni_path=None,
112+- links_prefix=None, netrules_path=None, dir=None):
113++ netrules_path=None, dir=None):
114+ if dir is None:
115+ dir = self.tmp_dir()
116+
117+@@ -1391,8 +1389,7 @@ class TestEniRoundTrip(CiTestCase):
118+ eni_path = 'etc/network/interfaces'
119+
120+ renderer = eni.Renderer(
121+- config={'eni_path': eni_path, 'links_path_prefix': links_prefix,
122+- 'netrules_path': netrules_path})
123++ config={'eni_path': eni_path, 'netrules_path': netrules_path})
124+
125+ renderer.render_network_state(ns, dir)
126+ return dir2dict(dir)
127diff --git a/debian/patches/cpick-11121fe4-systemd-make-cloud-final.service-run-before-apt-daily b/debian/patches/cpick-11121fe4-systemd-make-cloud-final.service-run-before-apt-daily
128new file mode 100644
129index 0000000..643a3e2
130--- /dev/null
131+++ b/debian/patches/cpick-11121fe4-systemd-make-cloud-final.service-run-before-apt-daily
132@@ -0,0 +1,33 @@
133+From 11121fe4d5af0554140d88685029fa248fa0c7c9 Mon Sep 17 00:00:00 2001
134+From: Scott Moser <smoser@brickies.net>
135+Date: Mon, 12 Jun 2017 14:10:58 -0400
136+Subject: [PATCH] systemd: make cloud-final.service run before apt daily
137+ services.
138+
139+This changes all cloud-init systemd units to run 'Before' the apt processes
140+that run daily and may cause a lock on the apt database.
141+
142+apt-daily-upgrade.service contains 'After=apt-daily.service'.
143+Thus following order is enforced, so we can just be 'Before' the first.
144+ apt-daily.service
145+ apt-daily-upgrade.service
146+
147+Note that this means only that apt-daily* will not run until
148+cloud-init has entirely finished. Any other processes running apt-get
149+operations are still affected by the global lock.
150+
151+LP: #1693361
152+---
153+ systemd/cloud-final.service | 1 +
154+ 1 file changed, 1 insertion(+)
155+
156+--- a/systemd/cloud-final.service
157++++ b/systemd/cloud-final.service
158+@@ -2,6 +2,7 @@
159+ Description=Execute cloud user/final scripts
160+ After=network-online.target cloud-config.service rc-local.service multi-user.target
161+ Wants=network-online.target cloud-config.service
162++Before=apt-daily.service
163+
164+ [Service]
165+ Type=oneshot
166diff --git a/debian/patches/cpick-1cd4323b-azure-remove-accidental-duplicate-line-in-merge b/debian/patches/cpick-1cd4323b-azure-remove-accidental-duplicate-line-in-merge
167new file mode 100644
168index 0000000..2ddf83e
169--- /dev/null
170+++ b/debian/patches/cpick-1cd4323b-azure-remove-accidental-duplicate-line-in-merge
171@@ -0,0 +1,22 @@
172+From 1cd4323b940408aa34dcaa01bd8a7ed43d9a966a Mon Sep 17 00:00:00 2001
173+From: Scott Moser <smoser@brickies.net>
174+Date: Thu, 1 Jun 2017 12:40:12 -0400
175+Subject: [PATCH] azure: remove accidental duplicate line in merge.
176+
177+In previous commit I inadvertantly left two calls to
178+ asset_tag = util.read_dmi_data('chassis-asset-tag')
179+The second did not do anything useful. Thus, remove it.
180+---
181+ cloudinit/sources/DataSourceAzure.py | 1 -
182+ 1 file changed, 1 deletion(-)
183+
184+--- a/cloudinit/sources/DataSourceAzure.py
185++++ b/cloudinit/sources/DataSourceAzure.py
186+@@ -326,7 +326,6 @@ class DataSourceAzureNet(sources.DataSou
187+ if asset_tag != AZURE_CHASSIS_ASSET_TAG:
188+ LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
189+ return False
190+- asset_tag = util.read_dmi_data('chassis-asset-tag')
191+ ddir = self.ds_cfg['data_dir']
192+
193+ candidates = [self.seed_dir]
194diff --git a/debian/patches/cpick-5fb49bac-azure-identify-platform-by-well-known-value-in-chassis b/debian/patches/cpick-5fb49bac-azure-identify-platform-by-well-known-value-in-chassis
195new file mode 100644
196index 0000000..4bcda2d
197--- /dev/null
198+++ b/debian/patches/cpick-5fb49bac-azure-identify-platform-by-well-known-value-in-chassis
199@@ -0,0 +1,338 @@
200+From 5fb49bacf7441d8d20a7b4e0e7008ca586f5ebab Mon Sep 17 00:00:00 2001
201+From: Chad Smith <chad.smith@canonical.com>
202+Date: Tue, 30 May 2017 10:28:05 -0600
203+Subject: [PATCH] azure: identify platform by well known value in chassis asset
204+ tag.
205+
206+Azure sets a known chassis asset tag to 7783-7084-3265-9085-8269-3286-77.
207+We can inspect this in both ds-identify and DataSource.get_data to
208+determine whether we are on Azure.
209+
210+Added unit tests to cover these changes
211+and some minor tweaks to Exception error message content to give more
212+context on malformed or missing ovf-env.xml files.
213+
214+LP: #1693939
215+---
216+ cloudinit/sources/DataSourceAzure.py | 9 +++-
217+ tests/unittests/test_datasource/test_azure.py | 66 +++++++++++++++++++++++++--
218+ tests/unittests/test_ds_identify.py | 39 ++++++++++++++++
219+ tools/ds-identify | 35 +++++++++-----
220+ 4 files changed, 134 insertions(+), 15 deletions(-)
221+
222+--- a/cloudinit/sources/DataSourceAzure.py
223++++ b/cloudinit/sources/DataSourceAzure.py
224+@@ -36,6 +36,8 @@ RESOURCE_DISK_PATH = '/dev/disk/cloud/az
225+ DEFAULT_PRIMARY_NIC = 'eth0'
226+ LEASE_FILE = '/var/lib/dhcp/dhclient.eth0.leases'
227+ DEFAULT_FS = 'ext4'
228++# DMI chassis-asset-tag is set static for all azure instances
229++AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
230+
231+
232+ def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):
233+@@ -320,6 +322,11 @@ class DataSourceAzureNet(sources.DataSou
234+ # azure removes/ejects the cdrom containing the ovf-env.xml
235+ # file on reboot. So, in order to successfully reboot we
236+ # need to look in the datadir and consider that valid
237++ asset_tag = util.read_dmi_data('chassis-asset-tag')
238++ if asset_tag != AZURE_CHASSIS_ASSET_TAG:
239++ LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
240++ return False
241++ asset_tag = util.read_dmi_data('chassis-asset-tag')
242+ ddir = self.ds_cfg['data_dir']
243+
244+ candidates = [self.seed_dir]
245+@@ -694,7 +701,7 @@ def read_azure_ovf(contents):
246+ try:
247+ dom = minidom.parseString(contents)
248+ except Exception as e:
249+- raise BrokenAzureDataSource("invalid xml: %s" % e)
250++ raise BrokenAzureDataSource("Invalid ovf-env.xml: %s" % e)
251+
252+ results = find_child(dom.documentElement,
253+ lambda n: n.localName == "ProvisioningSection")
254+--- a/tests/unittests/test_datasource/test_azure.py
255++++ b/tests/unittests/test_datasource/test_azure.py
256+@@ -76,7 +76,9 @@ def construct_valid_ovf_env(data=None, p
257+ return content
258+
259+
260+-class TestAzureDataSource(TestCase):
261++class TestAzureDataSource(CiTestCase):
262++
263++ with_logs = True
264+
265+ def setUp(self):
266+ super(TestAzureDataSource, self).setUp()
267+@@ -160,6 +162,12 @@ scbus-1 on xpt0 bus 0
268+
269+ self.instance_id = 'test-instance-id'
270+
271++ def _dmi_mocks(key):
272++ if key == 'system-uuid':
273++ return self.instance_id
274++ elif key == 'chassis-asset-tag':
275++ return '7783-7084-3265-9085-8269-3286-77'
276++
277+ self.apply_patches([
278+ (dsaz, 'list_possible_azure_ds_devs', dsdevs),
279+ (dsaz, 'invoke_agent', _invoke_agent),
280+@@ -170,7 +178,7 @@ scbus-1 on xpt0 bus 0
281+ (dsaz, 'set_hostname', mock.MagicMock()),
282+ (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
283+ (dsaz.util, 'read_dmi_data', mock.MagicMock(
284+- return_value=self.instance_id)),
285++ side_effect=_dmi_mocks)),
286+ ])
287+
288+ dsrc = dsaz.DataSourceAzureNet(
289+@@ -241,6 +249,23 @@ fdescfs /dev/fd fdes
290+ res = get_path_dev_freebsd('/etc', mnt_list)
291+ self.assertIsNotNone(res)
292+
293++ @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')
294++ def test_non_azure_dmi_chassis_asset_tag(self, m_read_dmi_data):
295++ """Report non-azure when DMI's chassis asset tag doesn't match.
296++
297++ Return False when the asset tag doesn't match Azure's static
298++ AZURE_CHASSIS_ASSET_TAG.
299++ """
300++ # Return a non-matching asset tag value
301++ nonazure_tag = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'
302++ m_read_dmi_data.return_value = nonazure_tag
303++ dsrc = dsaz.DataSourceAzureNet(
304++ {}, distro=None, paths=self.paths)
305++ self.assertFalse(dsrc.get_data())
306++ self.assertEqual(
307++ "Non-Azure DMI asset tag '{0}' discovered.\n".format(nonazure_tag),
308++ self.logs.getvalue())
309++
310+ def test_basic_seed_dir(self):
311+ odata = {'HostName': "myhost", 'UserName': "myuser"}
312+ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
313+@@ -531,9 +556,17 @@ class TestAzureBounce(TestCase):
314+ self.patches.enter_context(
315+ mock.patch.object(dsaz, 'get_metadata_from_fabric',
316+ mock.MagicMock(return_value={})))
317++
318++ def _dmi_mocks(key):
319++ if key == 'system-uuid':
320++ return 'test-instance-id'
321++ elif key == 'chassis-asset-tag':
322++ return '7783-7084-3265-9085-8269-3286-77'
323++ raise RuntimeError('should not get here')
324++
325+ self.patches.enter_context(
326+ mock.patch.object(dsaz.util, 'read_dmi_data',
327+- mock.MagicMock(return_value='test-instance-id')))
328++ mock.MagicMock(side_effect=_dmi_mocks)))
329+
330+ def setUp(self):
331+ super(TestAzureBounce, self).setUp()
332+@@ -696,6 +729,33 @@ class TestAzureBounce(TestCase):
333+ self.assertEqual(0, self.set_hostname.call_count)
334+
335+
336++class TestLoadAzureDsDir(CiTestCase):
337++ """Tests for load_azure_ds_dir."""
338++
339++ def setUp(self):
340++ self.source_dir = self.tmp_dir()
341++ super(TestLoadAzureDsDir, self).setUp()
342++
343++ def test_missing_ovf_env_xml_raises_non_azure_datasource_error(self):
344++ """load_azure_ds_dir raises an error When ovf-env.xml doesn't exit."""
345++ with self.assertRaises(dsaz.NonAzureDataSource) as context_manager:
346++ dsaz.load_azure_ds_dir(self.source_dir)
347++ self.assertEqual(
348++ 'No ovf-env file found',
349++ str(context_manager.exception))
350++
351++ def test_wb_invalid_ovf_env_xml_calls_read_azure_ovf(self):
352++ """load_azure_ds_dir calls read_azure_ovf to parse the xml."""
353++ ovf_path = os.path.join(self.source_dir, 'ovf-env.xml')
354++ with open(ovf_path, 'wb') as stream:
355++ stream.write(b'invalid xml')
356++ with self.assertRaises(dsaz.BrokenAzureDataSource) as context_manager:
357++ dsaz.load_azure_ds_dir(self.source_dir)
358++ self.assertEqual(
359++ 'Invalid ovf-env.xml: syntax error: line 1, column 0',
360++ str(context_manager.exception))
361++
362++
363+ class TestReadAzureOvf(TestCase):
364+ def test_invalid_xml_raises_non_azure_ds(self):
365+ invalid_xml = "<foo>" + construct_valid_ovf_env(data={})
366+--- a/tests/unittests/test_ds_identify.py
367++++ b/tests/unittests/test_ds_identify.py
368+@@ -39,9 +39,11 @@ RC_FOUND = 0
369+ RC_NOT_FOUND = 1
370+ DS_NONE = 'None'
371+
372++P_CHASSIS_ASSET_TAG = "sys/class/dmi/id/chassis_asset_tag"
373+ P_PRODUCT_NAME = "sys/class/dmi/id/product_name"
374+ P_PRODUCT_SERIAL = "sys/class/dmi/id/product_serial"
375+ P_PRODUCT_UUID = "sys/class/dmi/id/product_uuid"
376++P_SEED_DIR = "var/lib/cloud/seed"
377+ P_DSID_CFG = "etc/cloud/ds-identify.cfg"
378+
379+ MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}
380+@@ -160,6 +162,30 @@ class TestDsIdentify(CiTestCase):
381+ _print_run_output(rc, out, err, cfg, files)
382+ return rc, out, err, cfg, files
383+
384++ def test_wb_print_variables(self):
385++ """_print_info reports an array of discovered variables to stderr."""
386++ data = VALID_CFG['Azure-dmi-detection']
387++ _, _, err, _, _ = self._call_via_dict(data)
388++ expected_vars = [
389++ 'DMI_PRODUCT_NAME', 'DMI_SYS_VENDOR', 'DMI_PRODUCT_SERIAL',
390++ 'DMI_PRODUCT_UUID', 'PID_1_PRODUCT_NAME', 'DMI_CHASSIS_ASSET_TAG',
391++ 'FS_LABELS', 'KERNEL_CMDLINE', 'VIRT', 'UNAME_KERNEL_NAME',
392++ 'UNAME_KERNEL_RELEASE', 'UNAME_KERNEL_VERSION', 'UNAME_MACHINE',
393++ 'UNAME_NODENAME', 'UNAME_OPERATING_SYSTEM', 'DSNAME', 'DSLIST',
394++ 'MODE', 'ON_FOUND', 'ON_MAYBE', 'ON_NOTFOUND']
395++ for var in expected_vars:
396++ self.assertIn('{0}='.format(var), err)
397++
398++ def test_azure_dmi_detection_from_chassis_asset_tag(self):
399++ """Azure datasource is detected from DMI chassis-asset-tag"""
400++ self._test_ds_found('Azure-dmi-detection')
401++
402++ def test_azure_seed_file_detection(self):
403++ """Azure datasource is detected due to presence of a seed file.
404++
405++ The seed file tested is /var/lib/cloud/seed/azure/ovf-env.xml."""
406++ self._test_ds_found('Azure-seed-detection')
407++
408+ def test_aws_ec2_hvm(self):
409+ """EC2: hvm instances use dmi serial and uuid starting with 'ec2'."""
410+ self._test_ds_found('Ec2-hvm')
411+@@ -254,6 +280,19 @@ def _print_run_output(rc, out, err, cfg,
412+
413+
414+ VALID_CFG = {
415++ 'Azure-dmi-detection': {
416++ 'ds': 'Azure',
417++ 'files': {
418++ P_CHASSIS_ASSET_TAG: '7783-7084-3265-9085-8269-3286-77\n',
419++ }
420++ },
421++ 'Azure-seed-detection': {
422++ 'ds': 'Azure',
423++ 'files': {
424++ P_CHASSIS_ASSET_TAG: 'No-match\n',
425++ os.path.join(P_SEED_DIR, 'azure', 'ovf-env.xml'): 'present\n',
426++ }
427++ },
428+ 'Ec2-hvm': {
429+ 'ds': 'Ec2',
430+ 'mocks': [{'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}],
431+--- a/tools/ds-identify
432++++ b/tools/ds-identify
433+@@ -85,6 +85,7 @@ DI_MAIN=${DI_MAIN:-main}
434+
435+ DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
436+ DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
437++DI_DMI_CHASSIS_ASSET_TAG=""
438+ DI_DMI_PRODUCT_NAME=""
439+ DI_DMI_SYS_VENDOR=""
440+ DI_DMI_PRODUCT_SERIAL=""
441+@@ -258,6 +259,12 @@ read_kernel_cmdline() {
442+ DI_KERNEL_CMDLINE="$cmdline"
443+ }
444+
445++read_dmi_chassis_asset_tag() {
446++ cached "${DI_DMI_CHASSIS_ASSET_TAG}" && return
447++ get_dmi_field chassis_asset_tag
448++ DI_DMI_CHASSIS_ASSET_TAG="$_RET"
449++}
450++
451+ read_dmi_sys_vendor() {
452+ cached "${DI_DMI_SYS_VENDOR}" && return
453+ get_dmi_field sys_vendor
454+@@ -385,6 +392,14 @@ read_pid1_product_name() {
455+ DI_PID_1_PRODUCT_NAME="$product_name"
456+ }
457+
458++dmi_chassis_asset_tag_matches() {
459++ is_container && return 1
460++ case "${DI_DMI_CHASSIS_ASSET_TAG}" in
461++ $1) return 0;;
462++ esac
463++ return 1
464++}
465++
466+ dmi_product_name_matches() {
467+ is_container && return 1
468+ case "${DI_DMI_PRODUCT_NAME}" in
469+@@ -401,11 +416,6 @@ dmi_product_serial_matches() {
470+ return 1
471+ }
472+
473+-dmi_product_name_is() {
474+- is_container && return 1
475+- [ "${DI_DMI_PRODUCT_NAME}" = "$1" ]
476+-}
477+-
478+ dmi_sys_vendor_is() {
479+ is_container && return 1
480+ [ "${DI_DMI_SYS_VENDOR}" = "$1" ]
481+@@ -477,7 +487,7 @@ dscheck_CloudStack() {
482+
483+ dscheck_CloudSigma() {
484+ # http://paste.ubuntu.com/23624795/
485+- dmi_product_name_is "CloudSigma" && return $DS_FOUND
486++ dmi_product_name_matches "CloudSigma" && return $DS_FOUND
487+ return $DS_NOT_FOUND
488+ }
489+
490+@@ -653,6 +663,8 @@ dscheck_Azure() {
491+ # UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209"
492+ # TYPE="udf">/dev/sr0</device>
493+ #
494++ local azure_chassis="7783-7084-3265-9085-8269-3286-77"
495++ dmi_chassis_asset_tag_matches "${azure_chassis}" && return $DS_FOUND
496+ check_seed_dir azure ovf-env.xml && return ${DS_FOUND}
497+
498+ [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND}
499+@@ -785,7 +797,7 @@ dscheck_Ec2() {
500+ }
501+
502+ dscheck_GCE() {
503+- if dmi_product_name_is "Google Compute Engine"; then
504++ if dmi_product_name_matches "Google Compute Engine"; then
505+ return ${DS_FOUND}
506+ fi
507+ # product name is not guaranteed (LP: #1674861)
508+@@ -806,10 +818,10 @@ dscheck_OpenStack() {
509+ return ${DS_NOT_FOUND}
510+ fi
511+ local nova="OpenStack Nova" compute="OpenStack Compute"
512+- if dmi_product_name_is "$nova"; then
513++ if dmi_product_name_matches "$nova"; then
514+ return ${DS_FOUND}
515+ fi
516+- if dmi_product_name_is "$compute"; then
517++ if dmi_product_name_matches "$compute"; then
518+ # RDO installed nova (LP: #1675349).
519+ return ${DS_FOUND}
520+ fi
521+@@ -887,6 +899,7 @@ collect_info() {
522+ read_config
523+ read_datasource_list
524+ read_dmi_sys_vendor
525++ read_dmi_chassis_asset_tag
526+ read_dmi_product_name
527+ read_dmi_product_serial
528+ read_dmi_product_uuid
529+@@ -901,7 +914,7 @@ print_info() {
530+ _print_info() {
531+ local n="" v="" vars=""
532+ vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL"
533+- vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME"
534++ vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME DMI_CHASSIS_ASSET_TAG"
535+ vars="$vars FS_LABELS KERNEL_CMDLINE VIRT"
536+ vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_RELEASE UNAME_KERNEL_VERSION"
537+ vars="$vars UNAME_MACHINE UNAME_NODENAME UNAME_OPERATING_SYSTEM"
538diff --git a/debian/patches/cpick-ebc9ecbc-Azure-Add-network-config-Refactor-net-layer-to-handle b/debian/patches/cpick-ebc9ecbc-Azure-Add-network-config-Refactor-net-layer-to-handle
539new file mode 100644
540index 0000000..814f2ef
541--- /dev/null
542+++ b/debian/patches/cpick-ebc9ecbc-Azure-Add-network-config-Refactor-net-layer-to-handle
543@@ -0,0 +1,1474 @@
544+From ebc9ecbc8a76bdf511a456fb72339a7eb4c20568 Mon Sep 17 00:00:00 2001
545+From: Ryan Harper <ryan.harper@canonical.com>
546+Date: Tue, 20 Jun 2017 17:06:43 -0500
547+Subject: [PATCH] Azure: Add network-config, Refactor net layer to handle
548+ duplicate macs.
549+
550+On systems with network devices with duplicate mac addresses, cloud-init
551+will fail to rename the devices according to the specified network
552+configuration. Refactor net layer to search by device driver and device
553+id if available. Azure systems may have duplicate mac addresses by
554+design.
555+
556+Update Azure datasource to run at init-local time and let Azure datasource
557+generate a fallback networking config to handle advanced networking
558+configurations.
559+
560+Lastly, add a 'setup' method to the datasources that is called before
561+userdata/vendordata is processed but after networking is up. That is
562+used here on Azure to interact with the 'fabric'.
563+---
564+ cloudinit/cmd/main.py | 3 +
565+ cloudinit/net/__init__.py | 181 ++++++++--
566+ cloudinit/net/eni.py | 2 +
567+ cloudinit/net/renderer.py | 4 +-
568+ cloudinit/net/udev.py | 7 +-
569+ cloudinit/sources/DataSourceAzure.py | 114 +++++-
570+ cloudinit/sources/__init__.py | 15 +-
571+ cloudinit/stages.py | 5 +
572+ tests/unittests/test_datasource/test_azure.py | 174 +++++++--
573+ tests/unittests/test_datasource/test_common.py | 2 +-
574+ tests/unittests/test_net.py | 478 ++++++++++++++++++++++++-
575+ 11 files changed, 887 insertions(+), 98 deletions(-)
576+
577+--- a/cloudinit/cmd/main.py
578++++ b/cloudinit/cmd/main.py
579+@@ -373,6 +373,9 @@ def main_init(name, args):
580+ LOG.debug("[%s] %s is in local mode, will apply init modules now.",
581+ mode, init.datasource)
582+
583++ # Give the datasource a chance to use network resources.
584++ # This is used on Azure to communicate with the fabric over network.
585++ init.setup_datasource()
586+ # update fully realizes user-data (pulling in #include if necessary)
587+ init.update()
588+ # Stage 7
589+--- a/cloudinit/net/__init__.py
590++++ b/cloudinit/net/__init__.py
591+@@ -86,6 +86,10 @@ def is_bridge(devname):
592+ return os.path.exists(sys_dev_path(devname, "bridge"))
593+
594+
595++def is_bond(devname):
596++ return os.path.exists(sys_dev_path(devname, "bonding"))
597++
598++
599+ def is_vlan(devname):
600+ uevent = str(read_sys_net_safe(devname, "uevent"))
601+ return 'DEVTYPE=vlan' in uevent.splitlines()
602+@@ -113,6 +117,26 @@ def is_present(devname):
603+ return os.path.exists(sys_dev_path(devname))
604+
605+
606++def device_driver(devname):
607++ """Return the device driver for net device named 'devname'."""
608++ driver = None
609++ driver_path = sys_dev_path(devname, "device/driver")
610++ # driver is a symlink to the driver *dir*
611++ if os.path.islink(driver_path):
612++ driver = os.path.basename(os.readlink(driver_path))
613++
614++ return driver
615++
616++
617++def device_devid(devname):
618++ """Return the device id string for net device named 'devname'."""
619++ dev_id = read_sys_net_safe(devname, "device/device")
620++ if dev_id is False:
621++ return None
622++
623++ return dev_id
624++
625++
626+ def get_devicelist():
627+ return os.listdir(SYS_CLASS_NET)
628+
629+@@ -127,12 +151,21 @@ def is_disabled_cfg(cfg):
630+ return cfg.get('config') == "disabled"
631+
632+
633+-def generate_fallback_config():
634++def generate_fallback_config(blacklist_drivers=None, config_driver=None):
635+ """Determine which attached net dev is most likely to have a connection and
636+ generate network state to run dhcp on that interface"""
637++
638++ if not config_driver:
639++ config_driver = False
640++
641++ if not blacklist_drivers:
642++ blacklist_drivers = []
643++
644+ # get list of interfaces that could have connections
645+ invalid_interfaces = set(['lo'])
646+- potential_interfaces = set(get_devicelist())
647++ potential_interfaces = set([device for device in get_devicelist()
648++ if device_driver(device) not in
649++ blacklist_drivers])
650+ potential_interfaces = potential_interfaces.difference(invalid_interfaces)
651+ # sort into interfaces with carrier, interfaces which could have carrier,
652+ # and ignore interfaces that are definitely disconnected
653+@@ -144,6 +177,9 @@ def generate_fallback_config():
654+ if is_bridge(interface):
655+ # skip any bridges
656+ continue
657++ if is_bond(interface):
658++ # skip any bonds
659++ continue
660+ carrier = read_sys_net_int(interface, 'carrier')
661+ if carrier:
662+ connected.append(interface)
663+@@ -183,9 +219,18 @@ def generate_fallback_config():
664+ break
665+ if target_mac and target_name:
666+ nconf = {'config': [], 'version': 1}
667+- nconf['config'].append(
668+- {'type': 'physical', 'name': target_name,
669+- 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]})
670++ cfg = {'type': 'physical', 'name': target_name,
671++ 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}
672++ # inject the device driver name, dev_id into config if enabled and
673++ # device has a valid device driver value
674++ if config_driver:
675++ driver = device_driver(target_name)
676++ if driver:
677++ cfg['params'] = {
678++ 'driver': driver,
679++ 'device_id': device_devid(target_name),
680++ }
681++ nconf['config'].append(cfg)
682+ return nconf
683+ else:
684+ # can't read any interfaces addresses (or there are none); give up
685+@@ -206,10 +251,16 @@ def apply_network_config_names(netcfg, s
686+ if ent.get('type') != 'physical':
687+ continue
688+ mac = ent.get('mac_address')
689+- name = ent.get('name')
690+ if not mac:
691+ continue
692+- renames.append([mac, name])
693++ name = ent.get('name')
694++ driver = ent.get('params', {}).get('driver')
695++ device_id = ent.get('params', {}).get('device_id')
696++ if not driver:
697++ driver = device_driver(name)
698++ if not device_id:
699++ device_id = device_devid(name)
700++ renames.append([mac, name, driver, device_id])
701+
702+ return _rename_interfaces(renames)
703+
704+@@ -234,15 +285,27 @@ def _get_current_rename_info(check_downa
705+ """Collect information necessary for rename_interfaces.
706+
707+ returns a dictionary by mac address like:
708+- {mac:
709+- {'name': name
710+- 'up': boolean: is_up(name),
711++ {name:
712++ {
713+ 'downable': None or boolean indicating that the
714+- device has only automatically assigned ip addrs.}}
715++ device has only automatically assigned ip addrs.
716++ 'device_id': Device id value (if it has one)
717++ 'driver': Device driver (if it has one)
718++ 'mac': mac address
719++ 'name': name
720++ 'up': boolean: is_up(name)
721++ }}
722+ """
723+- bymac = {}
724+- for mac, name in get_interfaces_by_mac().items():
725+- bymac[mac] = {'name': name, 'up': is_up(name), 'downable': None}
726++ cur_info = {}
727++ for (name, mac, driver, device_id) in get_interfaces():
728++ cur_info[name] = {
729++ 'downable': None,
730++ 'device_id': device_id,
731++ 'driver': driver,
732++ 'mac': mac,
733++ 'name': name,
734++ 'up': is_up(name),
735++ }
736+
737+ if check_downable:
738+ nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
739+@@ -254,11 +317,11 @@ def _get_current_rename_info(check_downa
740+ for bytes_out in (ipv6, ipv4):
741+ nics_with_addresses.update(nmatch.findall(bytes_out))
742+
743+- for d in bymac.values():
744++ for d in cur_info.values():
745+ d['downable'] = (d['up'] is False or
746+ d['name'] not in nics_with_addresses)
747+
748+- return bymac
749++ return cur_info
750+
751+
752+ def _rename_interfaces(renames, strict_present=True, strict_busy=True,
753+@@ -271,15 +334,15 @@ def _rename_interfaces(renames, strict_p
754+ if current_info is None:
755+ current_info = _get_current_rename_info()
756+
757+- cur_bymac = {}
758+- for mac, data in current_info.items():
759++ cur_info = {}
760++ for name, data in current_info.items():
761+ cur = data.copy()
762+- cur['mac'] = mac
763+- cur_bymac[mac] = cur
764++ cur['name'] = name
765++ cur_info[name] = cur
766+
767+ def update_byname(bymac):
768+ return dict((data['name'], data)
769+- for data in bymac.values())
770++ for data in cur_info.values())
771+
772+ def rename(cur, new):
773+ util.subp(["ip", "link", "set", cur, "name", new], capture=True)
774+@@ -293,14 +356,48 @@ def _rename_interfaces(renames, strict_p
775+ ops = []
776+ errors = []
777+ ups = []
778+- cur_byname = update_byname(cur_bymac)
779++ cur_byname = update_byname(cur_info)
780+ tmpname_fmt = "cirename%d"
781+ tmpi = -1
782+
783+- for mac, new_name in renames:
784+- cur = cur_bymac.get(mac, {})
785+- cur_name = cur.get('name')
786++ def entry_match(data, mac, driver, device_id):
787++ """match if set and in data"""
788++ if mac and driver and device_id:
789++ return (data['mac'] == mac and
790++ data['driver'] == driver and
791++ data['device_id'] == device_id)
792++ elif mac and driver:
793++ return (data['mac'] == mac and
794++ data['driver'] == driver)
795++ elif mac:
796++ return (data['mac'] == mac)
797++
798++ return False
799++
800++ def find_entry(mac, driver, device_id):
801++ match = [data for data in cur_info.values()
802++ if entry_match(data, mac, driver, device_id)]
803++ if len(match):
804++ if len(match) > 1:
805++ msg = ('Failed to match a single device. Matched devices "%s"'
806++ ' with search values "(mac:%s driver:%s device_id:%s)"'
807++ % (match, mac, driver, device_id))
808++ raise ValueError(msg)
809++ return match[0]
810++
811++ return None
812++
813++ for mac, new_name, driver, device_id in renames:
814+ cur_ops = []
815++ cur = find_entry(mac, driver, device_id)
816++ if not cur:
817++ if strict_present:
818++ errors.append(
819++ "[nic not present] Cannot rename mac=%s to %s"
820++ ", not available." % (mac, new_name))
821++ continue
822++
823++ cur_name = cur.get('name')
824+ if cur_name == new_name:
825+ # nothing to do
826+ continue
827+@@ -340,13 +437,13 @@ def _rename_interfaces(renames, strict_p
828+
829+ cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
830+ target['name'] = tmp_name
831+- cur_byname = update_byname(cur_bymac)
832++ cur_byname = update_byname(cur_info)
833+ if target['up']:
834+ ups.append(("up", mac, new_name, (tmp_name,)))
835+
836+ cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
837+ cur['name'] = new_name
838+- cur_byname = update_byname(cur_bymac)
839++ cur_byname = update_byname(cur_info)
840+ ops += cur_ops
841+
842+ opmap = {'rename': rename, 'down': down, 'up': up}
843+@@ -415,6 +512,36 @@ def get_interfaces_by_mac():
844+ return ret
845+
846+
847++def get_interfaces():
848++ """Return list of interface tuples (name, mac, driver, device_id)
849++
850++ Bridges and any devices that have a 'stolen' mac are excluded."""
851++ try:
852++ devs = get_devicelist()
853++ except OSError as e:
854++ if e.errno == errno.ENOENT:
855++ devs = []
856++ else:
857++ raise
858++ ret = []
859++ empty_mac = '00:00:00:00:00:00'
860++ for name in devs:
861++ if not interface_has_own_mac(name):
862++ continue
863++ if is_bridge(name):
864++ continue
865++ if is_vlan(name):
866++ continue
867++ mac = get_interface_mac(name)
868++ # some devices may not have a mac (tun0)
869++ if not mac:
870++ continue
871++ if mac == empty_mac and name != 'lo':
872++ continue
873++ ret.append((name, mac, device_driver(name), device_devid(name)))
874++ return ret
875++
876++
877+ class RendererNotFoundError(RuntimeError):
878+ pass
879+
880+--- a/cloudinit/net/eni.py
881++++ b/cloudinit/net/eni.py
882+@@ -68,6 +68,8 @@ def _iface_add_attrs(iface, index):
883+ content = []
884+ ignore_map = [
885+ 'control',
886++ 'device_id',
887++ 'driver',
888+ 'index',
889+ 'inet',
890+ 'mode',
891+--- a/cloudinit/net/renderer.py
892++++ b/cloudinit/net/renderer.py
893+@@ -34,8 +34,10 @@ class Renderer(object):
894+ for iface in network_state.iter_interfaces(filter_by_physical):
895+ # for physical interfaces write out a persist net udev rule
896+ if 'name' in iface and iface.get('mac_address'):
897++ driver = iface.get('driver', None)
898+ content.write(generate_udev_rule(iface['name'],
899+- iface['mac_address']))
900++ iface['mac_address'],
901++ driver=driver))
902+ return content.getvalue()
903+
904+ @abc.abstractmethod
905+--- a/cloudinit/net/udev.py
906++++ b/cloudinit/net/udev.py
907+@@ -23,7 +23,7 @@ def compose_udev_setting(key, value):
908+ return '%s="%s"' % (key, value)
909+
910+
911+-def generate_udev_rule(interface, mac):
912++def generate_udev_rule(interface, mac, driver=None):
913+ """Return a udev rule to set the name of network interface with `mac`.
914+
915+ The rule ends up as a single line looking something like:
916+@@ -31,10 +31,13 @@ def generate_udev_rule(interface, mac):
917+ SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*",
918+ ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0"
919+ """
920++ if not driver:
921++ driver = '?*'
922++
923+ rule = ', '.join([
924+ compose_udev_equality('SUBSYSTEM', 'net'),
925+ compose_udev_equality('ACTION', 'add'),
926+- compose_udev_equality('DRIVERS', '?*'),
927++ compose_udev_equality('DRIVERS', driver),
928+ compose_udev_attr_equality('address', mac),
929+ compose_udev_setting('NAME', interface),
930+ ])
931+--- a/cloudinit/sources/DataSourceAzure.py
932++++ b/cloudinit/sources/DataSourceAzure.py
933+@@ -16,6 +16,7 @@ from xml.dom import minidom
934+ import xml.etree.ElementTree as ET
935+
936+ from cloudinit import log as logging
937++from cloudinit import net
938+ from cloudinit import sources
939+ from cloudinit.sources.helpers.azure import get_metadata_from_fabric
940+ from cloudinit import util
941+@@ -240,7 +241,9 @@ def temporary_hostname(temp_hostname, cf
942+ set_hostname(previous_hostname, hostname_command)
943+
944+
945+-class DataSourceAzureNet(sources.DataSource):
946++class DataSourceAzure(sources.DataSource):
947++ _negotiated = False
948++
949+ def __init__(self, sys_cfg, distro, paths):
950+ sources.DataSource.__init__(self, sys_cfg, distro, paths)
951+ self.seed_dir = os.path.join(paths.seed_dir, 'azure')
952+@@ -250,6 +253,7 @@ class DataSourceAzureNet(sources.DataSou
953+ util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
954+ BUILTIN_DS_CONFIG])
955+ self.dhclient_lease_file = self.ds_cfg.get('dhclient_lease_file')
956++ self._network_config = None
957+
958+ def __str__(self):
959+ root = sources.DataSource.__str__(self)
960+@@ -326,6 +330,7 @@ class DataSourceAzureNet(sources.DataSou
961+ if asset_tag != AZURE_CHASSIS_ASSET_TAG:
962+ LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
963+ return False
964++
965+ ddir = self.ds_cfg['data_dir']
966+
967+ candidates = [self.seed_dir]
968+@@ -370,13 +375,14 @@ class DataSourceAzureNet(sources.DataSou
969+ LOG.debug("using files cached in %s", ddir)
970+
971+ # azure / hyper-v provides random data here
972++ # TODO. find the seed on FreeBSD platform
973++ # now update ds_cfg to reflect contents pass in config
974+ if not util.is_FreeBSD():
975+ seed = util.load_file("/sys/firmware/acpi/tables/OEM0",
976+ quiet=True, decode=False)
977+ if seed:
978+ self.metadata['random_seed'] = seed
979+- # TODO. find the seed on FreeBSD platform
980+- # now update ds_cfg to reflect contents pass in config
981++
982+ user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
983+ self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
984+
985+@@ -384,6 +390,40 @@ class DataSourceAzureNet(sources.DataSou
986+ # the directory to be protected.
987+ write_files(ddir, files, dirmode=0o700)
988+
989++ self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
990++
991++ return True
992++
993++ def device_name_to_device(self, name):
994++ return self.ds_cfg['disk_aliases'].get(name)
995++
996++ def get_config_obj(self):
997++ return self.cfg
998++
999++ def check_instance_id(self, sys_cfg):
1000++ # quickly (local check only) if self.instance_id is still valid
1001++ return sources.instance_id_matches_system_uuid(self.get_instance_id())
1002++
1003++ def setup(self, is_new_instance):
1004++ if self._negotiated is False:
1005++ LOG.debug("negotiating for %s (new_instance=%s)",
1006++ self.get_instance_id(), is_new_instance)
1007++ fabric_data = self._negotiate()
1008++ LOG.debug("negotiating returned %s", fabric_data)
1009++ if fabric_data:
1010++ self.metadata.update(fabric_data)
1011++ self._negotiated = True
1012++ else:
1013++ LOG.debug("negotiating already done for %s",
1014++ self.get_instance_id())
1015++
1016++ def _negotiate(self):
1017++ """Negotiate with fabric and return data from it.
1018++
1019++ On success, returns a dictionary including 'public_keys'.
1020++ On failure, returns False.
1021++ """
1022++
1023+ if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
1024+ self.bounce_network_with_azure_hostname()
1025+
1026+@@ -393,31 +433,64 @@ class DataSourceAzureNet(sources.DataSou
1027+ else:
1028+ metadata_func = self.get_metadata_from_agent
1029+
1030++ LOG.debug("negotiating with fabric via agent command %s",
1031++ self.ds_cfg['agent_command'])
1032+ try:
1033+ fabric_data = metadata_func()
1034+ except Exception as exc:
1035+- LOG.info("Error communicating with Azure fabric; assume we aren't"
1036+- " on Azure.", exc_info=True)
1037++ LOG.warning(
1038++ "Error communicating with Azure fabric; You may experience."
1039++ "connectivity issues.", exc_info=True)
1040+ return False
1041+- self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
1042+- self.metadata.update(fabric_data)
1043+-
1044+- return True
1045+-
1046+- def device_name_to_device(self, name):
1047+- return self.ds_cfg['disk_aliases'].get(name)
1048+
1049+- def get_config_obj(self):
1050+- return self.cfg
1051+-
1052+- def check_instance_id(self, sys_cfg):
1053+- # quickly (local check only) if self.instance_id is still valid
1054+- return sources.instance_id_matches_system_uuid(self.get_instance_id())
1055++ return fabric_data
1056+
1057+ def activate(self, cfg, is_new_instance):
1058+ address_ephemeral_resize(is_new_instance=is_new_instance)
1059+ return
1060+
1061++ @property
1062++ def network_config(self):
1063++ """Generate a network config like net.generate_fallback_network() with
1064++ the following execptions.
1065++
1066++ 1. Probe the drivers of the net-devices present and inject them in
1067++ the network configuration under params: driver: <driver> value
1068++ 2. If the driver value is 'mlx4_core', the control mode should be
1069++ set to manual. The device will be later used to build a bond,
1070++ for now we want to ensure the device gets named but does not
1071++ break any network configuration
1072++ """
1073++ blacklist = ['mlx4_core']
1074++ if not self._network_config:
1075++ LOG.debug('Azure: generating fallback configuration')
1076++ # generate a network config, blacklist picking any mlx4_core devs
1077++ netconfig = net.generate_fallback_config(
1078++ blacklist_drivers=blacklist, config_driver=True)
1079++
1080++ # if we have any blacklisted devices, update the network_config to
1081++ # include the device, mac, and driver values, but with no ip
1082++ # config; this ensures udev rules are generated but won't affect
1083++ # ip configuration
1084++ bl_found = 0
1085++ for bl_dev in [dev for dev in net.get_devicelist()
1086++ if net.device_driver(dev) in blacklist]:
1087++ bl_found += 1
1088++ cfg = {
1089++ 'type': 'physical',
1090++ 'name': 'vf%d' % bl_found,
1091++ 'mac_address': net.get_interface_mac(bl_dev),
1092++ 'params': {
1093++ 'driver': net.device_driver(bl_dev),
1094++ 'device_id': net.device_devid(bl_dev),
1095++ },
1096++ }
1097++ netconfig['config'].append(cfg)
1098++
1099++ self._network_config = netconfig
1100++
1101++ return self._network_config
1102++
1103+
1104+ def _partitions_on_device(devpath, maxnum=16):
1105+ # return a list of tuples (ptnum, path) for each part on devpath
1106+@@ -840,9 +913,12 @@ class NonAzureDataSource(Exception):
1107+ pass
1108+
1109+
1110++# Legacy: Must be present in case we load an old pkl object
1111++DataSourceAzureNet = DataSourceAzure
1112++
1113+ # Used to match classes to dependencies
1114+ datasources = [
1115+- (DataSourceAzureNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
1116++ (DataSourceAzure, (sources.DEP_FILESYSTEM, )),
1117+ ]
1118+
1119+
1120+--- a/cloudinit/sources/__init__.py
1121++++ b/cloudinit/sources/__init__.py
1122+@@ -251,10 +251,23 @@ class DataSource(object):
1123+ def first_instance_boot(self):
1124+ return
1125+
1126++ def setup(self, is_new_instance):
1127++ """setup(is_new_instance)
1128++
1129++ This is called before user-data and vendor-data have been processed.
1130++
1131++ Unless the datasource has set mode to 'local', then networking
1132++ per 'fallback' or per 'network_config' will have been written and
1133++ brought up the OS at this point.
1134++ """
1135++ return
1136++
1137+ def activate(self, cfg, is_new_instance):
1138+ """activate(cfg, is_new_instance)
1139+
1140+- This is called before the init_modules will be called.
1141++ This is called before the init_modules will be called but after
1142++ the user-data and vendor-data have been fully processed.
1143++
1144+ The cfg is fully up to date config, it contains a merged view of
1145+ system config, datasource config, user config, vendor config.
1146+ It should be used rather than the sys_cfg passed to __init__.
1147+--- a/cloudinit/stages.py
1148++++ b/cloudinit/stages.py
1149+@@ -362,6 +362,11 @@ class Init(object):
1150+ self._store_userdata()
1151+ self._store_vendordata()
1152+
1153++ def setup_datasource(self):
1154++ if self.datasource is None:
1155++ raise RuntimeError("Datasource is None, cannot setup.")
1156++ self.datasource.setup(is_new_instance=self.is_new_instance())
1157++
1158+ def activate_datasource(self):
1159+ if self.datasource is None:
1160+ raise RuntimeError("Datasource is None, cannot activate.")
1161+--- a/tests/unittests/test_datasource/test_azure.py
1162++++ b/tests/unittests/test_datasource/test_azure.py
1163+@@ -181,13 +181,19 @@ scbus-1 on xpt0 bus 0
1164+ side_effect=_dmi_mocks)),
1165+ ])
1166+
1167+- dsrc = dsaz.DataSourceAzureNet(
1168++ dsrc = dsaz.DataSourceAzure(
1169+ data.get('sys_cfg', {}), distro=None, paths=self.paths)
1170+ if agent_command is not None:
1171+ dsrc.ds_cfg['agent_command'] = agent_command
1172+
1173+ return dsrc
1174+
1175++ def _get_and_setup(self, dsrc):
1176++ ret = dsrc.get_data()
1177++ if ret:
1178++ dsrc.setup(True)
1179++ return ret
1180++
1181+ def xml_equals(self, oxml, nxml):
1182+ """Compare two sets of XML to make sure they are equal"""
1183+
1184+@@ -259,7 +265,7 @@ fdescfs /dev/fd fdes
1185+ # Return a non-matching asset tag value
1186+ nonazure_tag = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'
1187+ m_read_dmi_data.return_value = nonazure_tag
1188+- dsrc = dsaz.DataSourceAzureNet(
1189++ dsrc = dsaz.DataSourceAzure(
1190+ {}, distro=None, paths=self.paths)
1191+ self.assertFalse(dsrc.get_data())
1192+ self.assertEqual(
1193+@@ -298,7 +304,7 @@ fdescfs /dev/fd fdes
1194+ data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
1195+
1196+ dsrc = self._get_ds(data)
1197+- ret = dsrc.get_data()
1198++ ret = self._get_and_setup(dsrc)
1199+ self.assertTrue(ret)
1200+ self.assertEqual(data['agent_invoked'], cfg['agent_command'])
1201+
1202+@@ -311,7 +317,7 @@ fdescfs /dev/fd fdes
1203+ data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
1204+
1205+ dsrc = self._get_ds(data)
1206+- ret = dsrc.get_data()
1207++ ret = self._get_and_setup(dsrc)
1208+ self.assertTrue(ret)
1209+ self.assertEqual(data['agent_invoked'], cfg['agent_command'])
1210+
1211+@@ -321,7 +327,7 @@ fdescfs /dev/fd fdes
1212+ 'sys_cfg': sys_cfg}
1213+
1214+ dsrc = self._get_ds(data)
1215+- ret = dsrc.get_data()
1216++ ret = self._get_and_setup(dsrc)
1217+ self.assertTrue(ret)
1218+ self.assertEqual(data['agent_invoked'], '_COMMAND')
1219+
1220+@@ -393,7 +399,7 @@ fdescfs /dev/fd fdes
1221+ pubkeys=pubkeys)}
1222+
1223+ dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
1224+- ret = dsrc.get_data()
1225++ ret = self._get_and_setup(dsrc)
1226+ self.assertTrue(ret)
1227+ for mypk in mypklist:
1228+ self.assertIn(mypk, dsrc.cfg['_pubkeys'])
1229+@@ -408,7 +414,7 @@ fdescfs /dev/fd fdes
1230+ pubkeys=pubkeys)}
1231+
1232+ dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
1233+- ret = dsrc.get_data()
1234++ ret = self._get_and_setup(dsrc)
1235+ self.assertTrue(ret)
1236+
1237+ for mypk in mypklist:
1238+@@ -424,7 +430,7 @@ fdescfs /dev/fd fdes
1239+ pubkeys=pubkeys)}
1240+
1241+ dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
1242+- ret = dsrc.get_data()
1243++ ret = self._get_and_setup(dsrc)
1244+ self.assertTrue(ret)
1245+
1246+ for mypk in mypklist:
1247+@@ -518,18 +524,20 @@ fdescfs /dev/fd fdes
1248+ dsrc.get_data()
1249+
1250+ def test_exception_fetching_fabric_data_doesnt_propagate(self):
1251+- ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
1252+- ds.ds_cfg['agent_command'] = '__builtin__'
1253++ """Errors communicating with fabric should warn, but return True."""
1254++ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
1255++ dsrc.ds_cfg['agent_command'] = '__builtin__'
1256+ self.get_metadata_from_fabric.side_effect = Exception
1257+- self.assertFalse(ds.get_data())
1258++ ret = self._get_and_setup(dsrc)
1259++ self.assertTrue(ret)
1260+
1261+ def test_fabric_data_included_in_metadata(self):
1262+- ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
1263+- ds.ds_cfg['agent_command'] = '__builtin__'
1264++ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
1265++ dsrc.ds_cfg['agent_command'] = '__builtin__'
1266+ self.get_metadata_from_fabric.return_value = {'test': 'value'}
1267+- ret = ds.get_data()
1268++ ret = self._get_and_setup(dsrc)
1269+ self.assertTrue(ret)
1270+- self.assertEqual('value', ds.metadata['test'])
1271++ self.assertEqual('value', dsrc.metadata['test'])
1272+
1273+ def test_instance_id_from_dmidecode_used(self):
1274+ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
1275+@@ -542,6 +550,84 @@ fdescfs /dev/fd fdes
1276+ ds.get_data()
1277+ self.assertEqual(self.instance_id, ds.metadata['instance-id'])
1278+
1279++ @mock.patch('cloudinit.net.get_interface_mac')
1280++ @mock.patch('cloudinit.net.get_devicelist')
1281++ @mock.patch('cloudinit.net.device_driver')
1282++ @mock.patch('cloudinit.net.generate_fallback_config')
1283++ def test_network_config(self, mock_fallback, mock_dd,
1284++ mock_devlist, mock_get_mac):
1285++ odata = {'HostName': "myhost", 'UserName': "myuser"}
1286++ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
1287++ 'sys_cfg': {}}
1288++
1289++ fallback_config = {
1290++ 'version': 1,
1291++ 'config': [{
1292++ 'type': 'physical', 'name': 'eth0',
1293++ 'mac_address': '00:11:22:33:44:55',
1294++ 'params': {'driver': 'hv_netsvc'},
1295++ 'subnets': [{'type': 'dhcp'}],
1296++ }]
1297++ }
1298++ mock_fallback.return_value = fallback_config
1299++
1300++ mock_devlist.return_value = ['eth0']
1301++ mock_dd.return_value = ['hv_netsvc']
1302++ mock_get_mac.return_value = '00:11:22:33:44:55'
1303++
1304++ dsrc = self._get_ds(data)
1305++ ret = dsrc.get_data()
1306++ self.assertTrue(ret)
1307++
1308++ netconfig = dsrc.network_config
1309++ self.assertEqual(netconfig, fallback_config)
1310++ mock_fallback.assert_called_with(blacklist_drivers=['mlx4_core'],
1311++ config_driver=True)
1312++
1313++ @mock.patch('cloudinit.net.get_interface_mac')
1314++ @mock.patch('cloudinit.net.get_devicelist')
1315++ @mock.patch('cloudinit.net.device_driver')
1316++ @mock.patch('cloudinit.net.generate_fallback_config')
1317++ def test_network_config_blacklist(self, mock_fallback, mock_dd,
1318++ mock_devlist, mock_get_mac):
1319++ odata = {'HostName': "myhost", 'UserName': "myuser"}
1320++ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
1321++ 'sys_cfg': {}}
1322++
1323++ fallback_config = {
1324++ 'version': 1,
1325++ 'config': [{
1326++ 'type': 'physical', 'name': 'eth0',
1327++ 'mac_address': '00:11:22:33:44:55',
1328++ 'params': {'driver': 'hv_netsvc'},
1329++ 'subnets': [{'type': 'dhcp'}],
1330++ }]
1331++ }
1332++ blacklist_config = {
1333++ 'type': 'physical',
1334++ 'name': 'eth1',
1335++ 'mac_address': '00:11:22:33:44:55',
1336++ 'params': {'driver': 'mlx4_core'}
1337++ }
1338++ mock_fallback.return_value = fallback_config
1339++
1340++ mock_devlist.return_value = ['eth0', 'eth1']
1341++ mock_dd.side_effect = [
1342++ 'hv_netsvc', # list composition, skipped
1343++ 'mlx4_core', # list composition, match
1344++ 'mlx4_core', # config get driver name
1345++ ]
1346++ mock_get_mac.return_value = '00:11:22:33:44:55'
1347++
1348++ dsrc = self._get_ds(data)
1349++ ret = dsrc.get_data()
1350++ self.assertTrue(ret)
1351++
1352++ netconfig = dsrc.network_config
1353++ expected_config = fallback_config
1354++ expected_config['config'].append(blacklist_config)
1355++ self.assertEqual(netconfig, expected_config)
1356++
1357+
1358+ class TestAzureBounce(TestCase):
1359+
1360+@@ -591,12 +677,18 @@ class TestAzureBounce(TestCase):
1361+ if ovfcontent is not None:
1362+ populate_dir(os.path.join(self.paths.seed_dir, "azure"),
1363+ {'ovf-env.xml': ovfcontent})
1364+- dsrc = dsaz.DataSourceAzureNet(
1365++ dsrc = dsaz.DataSourceAzure(
1366+ {}, distro=None, paths=self.paths)
1367+ if agent_command is not None:
1368+ dsrc.ds_cfg['agent_command'] = agent_command
1369+ return dsrc
1370+
1371++ def _get_and_setup(self, dsrc):
1372++ ret = dsrc.get_data()
1373++ if ret:
1374++ dsrc.setup(True)
1375++ return ret
1376++
1377+ def get_ovf_env_with_dscfg(self, hostname, cfg):
1378+ odata = {
1379+ 'HostName': hostname,
1380+@@ -640,17 +732,20 @@ class TestAzureBounce(TestCase):
1381+ host_name = 'unchanged-host-name'
1382+ self.get_hostname.return_value = host_name
1383+ cfg = {'hostname_bounce': {'policy': 'force'}}
1384+- self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg),
1385+- agent_command=['not', '__builtin__']).get_data()
1386++ dsrc = self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg),
1387++ agent_command=['not', '__builtin__'])
1388++ ret = self._get_and_setup(dsrc)
1389++ self.assertTrue(ret)
1390+ self.assertEqual(1, perform_hostname_bounce.call_count)
1391+
1392+ def test_different_hostnames_sets_hostname(self):
1393+ expected_hostname = 'azure-expected-host-name'
1394+ self.get_hostname.return_value = 'default-host-name'
1395+- self._get_ds(
1396++ dsrc = self._get_ds(
1397+ self.get_ovf_env_with_dscfg(expected_hostname, {}),
1398+- agent_command=['not', '__builtin__'],
1399+- ).get_data()
1400++ agent_command=['not', '__builtin__'])
1401++ ret = self._get_and_setup(dsrc)
1402++ self.assertTrue(ret)
1403+ self.assertEqual(expected_hostname,
1404+ self.set_hostname.call_args_list[0][0][0])
1405+
1406+@@ -659,19 +754,21 @@ class TestAzureBounce(TestCase):
1407+ self, perform_hostname_bounce):
1408+ expected_hostname = 'azure-expected-host-name'
1409+ self.get_hostname.return_value = 'default-host-name'
1410+- self._get_ds(
1411++ dsrc = self._get_ds(
1412+ self.get_ovf_env_with_dscfg(expected_hostname, {}),
1413+- agent_command=['not', '__builtin__'],
1414+- ).get_data()
1415++ agent_command=['not', '__builtin__'])
1416++ ret = self._get_and_setup(dsrc)
1417++ self.assertTrue(ret)
1418+ self.assertEqual(1, perform_hostname_bounce.call_count)
1419+
1420+ def test_different_hostnames_sets_hostname_back(self):
1421+ initial_host_name = 'default-host-name'
1422+ self.get_hostname.return_value = initial_host_name
1423+- self._get_ds(
1424++ dsrc = self._get_ds(
1425+ self.get_ovf_env_with_dscfg('some-host-name', {}),
1426+- agent_command=['not', '__builtin__'],
1427+- ).get_data()
1428++ agent_command=['not', '__builtin__'])
1429++ ret = self._get_and_setup(dsrc)
1430++ self.assertTrue(ret)
1431+ self.assertEqual(initial_host_name,
1432+ self.set_hostname.call_args_list[-1][0][0])
1433+
1434+@@ -681,10 +778,11 @@ class TestAzureBounce(TestCase):
1435+ perform_hostname_bounce.side_effect = Exception
1436+ initial_host_name = 'default-host-name'
1437+ self.get_hostname.return_value = initial_host_name
1438+- self._get_ds(
1439++ dsrc = self._get_ds(
1440+ self.get_ovf_env_with_dscfg('some-host-name', {}),
1441+- agent_command=['not', '__builtin__'],
1442+- ).get_data()
1443++ agent_command=['not', '__builtin__'])
1444++ ret = self._get_and_setup(dsrc)
1445++ self.assertTrue(ret)
1446+ self.assertEqual(initial_host_name,
1447+ self.set_hostname.call_args_list[-1][0][0])
1448+
1449+@@ -695,7 +793,9 @@ class TestAzureBounce(TestCase):
1450+ self.get_hostname.return_value = old_hostname
1451+ cfg = {'hostname_bounce': {'interface': interface, 'policy': 'force'}}
1452+ data = self.get_ovf_env_with_dscfg(hostname, cfg)
1453+- self._get_ds(data, agent_command=['not', '__builtin__']).get_data()
1454++ dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
1455++ ret = self._get_and_setup(dsrc)
1456++ self.assertTrue(ret)
1457+ self.assertEqual(1, self.subp.call_count)
1458+ bounce_env = self.subp.call_args[1]['env']
1459+ self.assertEqual(interface, bounce_env['interface'])
1460+@@ -707,7 +807,9 @@ class TestAzureBounce(TestCase):
1461+ dsaz.BUILTIN_DS_CONFIG['hostname_bounce']['command'] = cmd
1462+ cfg = {'hostname_bounce': {'policy': 'force'}}
1463+ data = self.get_ovf_env_with_dscfg('some-hostname', cfg)
1464+- self._get_ds(data, agent_command=['not', '__builtin__']).get_data()
1465++ dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
1466++ ret = self._get_and_setup(dsrc)
1467++ self.assertTrue(ret)
1468+ self.assertEqual(1, self.subp.call_count)
1469+ bounce_args = self.subp.call_args[1]['args']
1470+ self.assertEqual(cmd, bounce_args)
1471+@@ -963,4 +1065,12 @@ class TestCanDevBeReformatted(CiTestCase
1472+ self.assertEqual(False, value)
1473+ self.assertIn("3 or more", msg.lower())
1474+
1475++
1476++class TestAzureNetExists(CiTestCase):
1477++ def test_azure_net_must_exist_for_legacy_objpkl(self):
1478++ """DataSourceAzureNet must exist for old obj.pkl files
1479++ that reference it."""
1480++ self.assertTrue(hasattr(dsaz, "DataSourceAzureNet"))
1481++
1482++
1483+ # vi: ts=4 expandtab
1484+--- a/tests/unittests/test_datasource/test_common.py
1485++++ b/tests/unittests/test_datasource/test_common.py
1486+@@ -26,6 +26,7 @@ from cloudinit.sources import DataSource
1487+ from .. import helpers as test_helpers
1488+
1489+ DEFAULT_LOCAL = [
1490++ Azure.DataSourceAzure,
1491+ CloudSigma.DataSourceCloudSigma,
1492+ ConfigDrive.DataSourceConfigDrive,
1493+ DigitalOcean.DataSourceDigitalOcean,
1494+@@ -37,7 +38,6 @@ DEFAULT_LOCAL = [
1495+
1496+ DEFAULT_NETWORK = [
1497+ AltCloud.DataSourceAltCloud,
1498+- Azure.DataSourceAzureNet,
1499+ Bigstep.DataSourceBigstep,
1500+ CloudStack.DataSourceCloudStack,
1501+ DSNone.DataSourceNone,
1502+--- a/tests/unittests/test_net.py
1503++++ b/tests/unittests/test_net.py
1504+@@ -789,38 +789,176 @@ CONFIG_V1_EXPLICIT_LOOPBACK = {
1505+ 'subnets': [{'control': 'auto', 'type': 'loopback'}]},
1506+ ]}
1507+
1508++DEFAULT_DEV_ATTRS = {
1509++ 'eth1000': {
1510++ "bridge": False,
1511++ "carrier": False,
1512++ "dormant": False,
1513++ "operstate": "down",
1514++ "address": "07-1C-C6-75-A4-BE",
1515++ "device/driver": None,
1516++ "device/device": None,
1517++ }
1518++}
1519++
1520+
1521+ def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
1522+- mock_sys_dev_path):
1523+- mock_get_devicelist.return_value = ['eth1000']
1524+- dev_characteristics = {
1525+- 'eth1000': {
1526+- "bridge": False,
1527+- "carrier": False,
1528+- "dormant": False,
1529+- "operstate": "down",
1530+- "address": "07-1C-C6-75-A4-BE",
1531+- }
1532+- }
1533++ mock_sys_dev_path, dev_attrs=None):
1534++ if not dev_attrs:
1535++ dev_attrs = DEFAULT_DEV_ATTRS
1536++
1537++ mock_get_devicelist.return_value = dev_attrs.keys()
1538+
1539+ def fake_read(devname, path, translate=None,
1540+ on_enoent=None, on_keyerror=None,
1541+ on_einval=None):
1542+- return dev_characteristics[devname][path]
1543++ return dev_attrs[devname][path]
1544+
1545+ mock_read_sys_net.side_effect = fake_read
1546+
1547+ def sys_dev_path(devname, path=""):
1548+- return tmp_dir + devname + "/" + path
1549++ return tmp_dir + "/" + devname + "/" + path
1550+
1551+- for dev in dev_characteristics:
1552++ for dev in dev_attrs:
1553+ os.makedirs(os.path.join(tmp_dir, dev))
1554+ with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
1555+- fh.write("down")
1556++ fh.write(dev_attrs[dev]['operstate'])
1557++ os.makedirs(os.path.join(tmp_dir, dev, "device"))
1558++ for key in ['device/driver']:
1559++ if key in dev_attrs[dev] and dev_attrs[dev][key]:
1560++ target = dev_attrs[dev][key]
1561++ link = os.path.join(tmp_dir, dev, key)
1562++ print('symlink %s -> %s' % (link, target))
1563++ os.symlink(target, link)
1564+
1565+ mock_sys_dev_path.side_effect = sys_dev_path
1566+
1567+
1568++class TestGenerateFallbackConfig(CiTestCase):
1569++
1570++ @mock.patch("cloudinit.net.sys_dev_path")
1571++ @mock.patch("cloudinit.net.read_sys_net")
1572++ @mock.patch("cloudinit.net.get_devicelist")
1573++ def test_device_driver(self, mock_get_devicelist, mock_read_sys_net,
1574++ mock_sys_dev_path):
1575++ devices = {
1576++ 'eth0': {
1577++ 'bridge': False, 'carrier': False, 'dormant': False,
1578++ 'operstate': 'down', 'address': '00:11:22:33:44:55',
1579++ 'device/driver': 'hv_netsvc', 'device/device': '0x3'},
1580++ 'eth1': {
1581++ 'bridge': False, 'carrier': False, 'dormant': False,
1582++ 'operstate': 'down', 'address': '00:11:22:33:44:55',
1583++ 'device/driver': 'mlx4_core', 'device/device': '0x7'},
1584++ }
1585++
1586++ tmp_dir = self.tmp_dir()
1587++ _setup_test(tmp_dir, mock_get_devicelist,
1588++ mock_read_sys_net, mock_sys_dev_path,
1589++ dev_attrs=devices)
1590++
1591++ network_cfg = net.generate_fallback_config(config_driver=True)
1592++ ns = network_state.parse_net_config_data(network_cfg,
1593++ skip_broken=False)
1594++
1595++ render_dir = os.path.join(tmp_dir, "render")
1596++ os.makedirs(render_dir)
1597++
1598++ # don't set rulepath so eni writes them
1599++ renderer = eni.Renderer(
1600++ {'eni_path': 'interfaces', 'netrules_path': 'netrules'})
1601++ renderer.render_network_state(ns, render_dir)
1602++
1603++ self.assertTrue(os.path.exists(os.path.join(render_dir,
1604++ 'interfaces')))
1605++ with open(os.path.join(render_dir, 'interfaces')) as fh:
1606++ contents = fh.read()
1607++ print(contents)
1608++ expected = """
1609++auto lo
1610++iface lo inet loopback
1611++
1612++auto eth0
1613++iface eth0 inet dhcp
1614++"""
1615++ self.assertEqual(expected.lstrip(), contents.lstrip())
1616++
1617++ self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules')))
1618++ with open(os.path.join(render_dir, 'netrules')) as fh:
1619++ contents = fh.read()
1620++ print(contents)
1621++ expected_rule = [
1622++ 'SUBSYSTEM=="net"',
1623++ 'ACTION=="add"',
1624++ 'DRIVERS=="hv_netsvc"',
1625++ 'ATTR{address}=="00:11:22:33:44:55"',
1626++ 'NAME="eth0"',
1627++ ]
1628++ self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip())
1629++
1630++ @mock.patch("cloudinit.net.sys_dev_path")
1631++ @mock.patch("cloudinit.net.read_sys_net")
1632++ @mock.patch("cloudinit.net.get_devicelist")
1633++ def test_device_driver_blacklist(self, mock_get_devicelist,
1634++ mock_read_sys_net, mock_sys_dev_path):
1635++ devices = {
1636++ 'eth1': {
1637++ 'bridge': False, 'carrier': False, 'dormant': False,
1638++ 'operstate': 'down', 'address': '00:11:22:33:44:55',
1639++ 'device/driver': 'hv_netsvc', 'device/device': '0x3'},
1640++ 'eth0': {
1641++ 'bridge': False, 'carrier': False, 'dormant': False,
1642++ 'operstate': 'down', 'address': '00:11:22:33:44:55',
1643++ 'device/driver': 'mlx4_core', 'device/device': '0x7'},
1644++ }
1645++
1646++ tmp_dir = self.tmp_dir()
1647++ _setup_test(tmp_dir, mock_get_devicelist,
1648++ mock_read_sys_net, mock_sys_dev_path,
1649++ dev_attrs=devices)
1650++
1651++ blacklist = ['mlx4_core']
1652++ network_cfg = net.generate_fallback_config(blacklist_drivers=blacklist,
1653++ config_driver=True)
1654++ ns = network_state.parse_net_config_data(network_cfg,
1655++ skip_broken=False)
1656++
1657++ render_dir = os.path.join(tmp_dir, "render")
1658++ os.makedirs(render_dir)
1659++
1660++ # don't set rulepath so eni writes them
1661++ renderer = eni.Renderer(
1662++ {'eni_path': 'interfaces', 'netrules_path': 'netrules'})
1663++ renderer.render_network_state(ns, render_dir)
1664++
1665++ self.assertTrue(os.path.exists(os.path.join(render_dir,
1666++ 'interfaces')))
1667++ with open(os.path.join(render_dir, 'interfaces')) as fh:
1668++ contents = fh.read()
1669++ print(contents)
1670++ expected = """
1671++auto lo
1672++iface lo inet loopback
1673++
1674++auto eth1
1675++iface eth1 inet dhcp
1676++"""
1677++ self.assertEqual(expected.lstrip(), contents.lstrip())
1678++
1679++ self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules')))
1680++ with open(os.path.join(render_dir, 'netrules')) as fh:
1681++ contents = fh.read()
1682++ print(contents)
1683++ expected_rule = [
1684++ 'SUBSYSTEM=="net"',
1685++ 'ACTION=="add"',
1686++ 'DRIVERS=="hv_netsvc"',
1687++ 'ATTR{address}=="00:11:22:33:44:55"',
1688++ 'NAME="eth1"',
1689++ ]
1690++ self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip())
1691++
1692++
1693+ class TestSysConfigRendering(CiTestCase):
1694+
1695+ @mock.patch("cloudinit.net.sys_dev_path")
1696+@@ -1513,6 +1651,118 @@ class TestNetRenderers(CiTestCase):
1697+ priority=['sysconfig', 'eni'])
1698+
1699+
1700++class TestGetInterfaces(CiTestCase):
1701++ _data = {'bonds': ['bond1'],
1702++ 'bridges': ['bridge1'],
1703++ 'vlans': ['bond1.101'],
1704++ 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1',
1705++ 'bond1.101', 'lo', 'eth1'],
1706++ 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01',
1707++ 'enp0s2': 'aa:aa:aa:aa:aa:02',
1708++ 'bond1': 'aa:aa:aa:aa:aa:01',
1709++ 'bond1.101': 'aa:aa:aa:aa:aa:01',
1710++ 'bridge1': 'aa:aa:aa:aa:aa:03',
1711++ 'bridge1-nic': 'aa:aa:aa:aa:aa:03',
1712++ 'lo': '00:00:00:00:00:00',
1713++ 'greptap0': '00:00:00:00:00:00',
1714++ 'eth1': 'aa:aa:aa:aa:aa:01',
1715++ 'tun0': None},
1716++ 'drivers': {'enp0s1': 'virtio_net',
1717++ 'enp0s2': 'e1000',
1718++ 'bond1': None,
1719++ 'bond1.101': None,
1720++ 'bridge1': None,
1721++ 'bridge1-nic': None,
1722++ 'lo': None,
1723++ 'greptap0': None,
1724++ 'eth1': 'mlx4_core',
1725++ 'tun0': None}}
1726++ data = {}
1727++
1728++ def _se_get_devicelist(self):
1729++ return list(self.data['devices'])
1730++
1731++ def _se_device_driver(self, name):
1732++ return self.data['drivers'][name]
1733++
1734++ def _se_device_devid(self, name):
1735++ return '0x%s' % sorted(list(self.data['drivers'].keys())).index(name)
1736++
1737++ def _se_get_interface_mac(self, name):
1738++ return self.data['macs'][name]
1739++
1740++ def _se_is_bridge(self, name):
1741++ return name in self.data['bridges']
1742++
1743++ def _se_is_vlan(self, name):
1744++ return name in self.data['vlans']
1745++
1746++ def _se_interface_has_own_mac(self, name):
1747++ return name in self.data['own_macs']
1748++
1749++ def _mock_setup(self):
1750++ self.data = copy.deepcopy(self._data)
1751++ self.data['devices'] = set(list(self.data['macs'].keys()))
1752++ mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge',
1753++ 'interface_has_own_mac', 'is_vlan', 'device_driver',
1754++ 'device_devid')
1755++ self.mocks = {}
1756++ for n in mocks:
1757++ m = mock.patch('cloudinit.net.' + n,
1758++ side_effect=getattr(self, '_se_' + n))
1759++ self.addCleanup(m.stop)
1760++ self.mocks[n] = m.start()
1761++
1762++ def test_gi_includes_duplicate_macs(self):
1763++ self._mock_setup()
1764++ ret = net.get_interfaces()
1765++
1766++ self.assertIn('enp0s1', self._se_get_devicelist())
1767++ self.assertIn('eth1', self._se_get_devicelist())
1768++ found = [ent for ent in ret if 'aa:aa:aa:aa:aa:01' in ent]
1769++ self.assertEqual(len(found), 2)
1770++
1771++ def test_gi_excludes_any_without_mac_address(self):
1772++ self._mock_setup()
1773++ ret = net.get_interfaces()
1774++
1775++ self.assertIn('tun0', self._se_get_devicelist())
1776++ found = [ent for ent in ret if 'tun0' in ent]
1777++ self.assertEqual(len(found), 0)
1778++
1779++ def test_gi_excludes_stolen_macs(self):
1780++ self._mock_setup()
1781++ ret = net.get_interfaces()
1782++ self.mocks['interface_has_own_mac'].assert_has_calls(
1783++ [mock.call('enp0s1'), mock.call('bond1')], any_order=True)
1784++ expected = [
1785++ ('enp0s2', 'aa:aa:aa:aa:aa:02', 'e1000', '0x5'),
1786++ ('enp0s1', 'aa:aa:aa:aa:aa:01', 'virtio_net', '0x4'),
1787++ ('eth1', 'aa:aa:aa:aa:aa:01', 'mlx4_core', '0x6'),
1788++ ('lo', '00:00:00:00:00:00', None, '0x8'),
1789++ ('bridge1-nic', 'aa:aa:aa:aa:aa:03', None, '0x3'),
1790++ ]
1791++ self.assertEqual(sorted(expected), sorted(ret))
1792++
1793++ def test_gi_excludes_bridges(self):
1794++ self._mock_setup()
1795++ # add a device 'b1', make all return they have their "own mac",
1796++ # set everything other than 'b1' to be a bridge.
1797++ # then expect b1 is the only thing left.
1798++ self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1'
1799++ self.data['drivers']['b1'] = None
1800++ self.data['devices'].add('b1')
1801++ self.data['bonds'] = []
1802++ self.data['own_macs'] = self.data['devices']
1803++ self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"]
1804++ ret = net.get_interfaces()
1805++ self.assertEqual([('b1', 'aa:aa:aa:aa:aa:b1', None, '0x0')], ret)
1806++ self.mocks['is_bridge'].assert_has_calls(
1807++ [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'),
1808++ mock.call('b1')],
1809++ any_order=True)
1810++
1811++
1812+ class TestGetInterfacesByMac(CiTestCase):
1813+ _data = {'bonds': ['bond1'],
1814+ 'bridges': ['bridge1'],
1815+@@ -1631,4 +1881,202 @@ def _gzip_data(data):
1816+ gzfp.close()
1817+ return iobuf.getvalue()
1818+
1819++
1820++class TestRenameInterfaces(CiTestCase):
1821++
1822++ @mock.patch('cloudinit.util.subp')
1823++ def test_rename_all(self, mock_subp):
1824++ renames = [
1825++ ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'),
1826++ ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'),
1827++ ]
1828++ current_info = {
1829++ 'ens3': {
1830++ 'downable': True,
1831++ 'device_id': '0x3',
1832++ 'driver': 'virtio_net',
1833++ 'mac': '00:11:22:33:44:55',
1834++ 'name': 'ens3',
1835++ 'up': False},
1836++ 'ens5': {
1837++ 'downable': True,
1838++ 'device_id': '0x5',
1839++ 'driver': 'virtio_net',
1840++ 'mac': '00:11:22:33:44:aa',
1841++ 'name': 'ens5',
1842++ 'up': False},
1843++ }
1844++ net._rename_interfaces(renames, current_info=current_info)
1845++ print(mock_subp.call_args_list)
1846++ mock_subp.assert_has_calls([
1847++ mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'],
1848++ capture=True),
1849++ mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'],
1850++ capture=True),
1851++ ])
1852++
1853++ @mock.patch('cloudinit.util.subp')
1854++ def test_rename_no_driver_no_device_id(self, mock_subp):
1855++ renames = [
1856++ ('00:11:22:33:44:55', 'interface0', None, None),
1857++ ('00:11:22:33:44:aa', 'interface1', None, None),
1858++ ]
1859++ current_info = {
1860++ 'eth0': {
1861++ 'downable': True,
1862++ 'device_id': None,
1863++ 'driver': None,
1864++ 'mac': '00:11:22:33:44:55',
1865++ 'name': 'eth0',
1866++ 'up': False},
1867++ 'eth1': {
1868++ 'downable': True,
1869++ 'device_id': None,
1870++ 'driver': None,
1871++ 'mac': '00:11:22:33:44:aa',
1872++ 'name': 'eth1',
1873++ 'up': False},
1874++ }
1875++ net._rename_interfaces(renames, current_info=current_info)
1876++ print(mock_subp.call_args_list)
1877++ mock_subp.assert_has_calls([
1878++ mock.call(['ip', 'link', 'set', 'eth0', 'name', 'interface0'],
1879++ capture=True),
1880++ mock.call(['ip', 'link', 'set', 'eth1', 'name', 'interface1'],
1881++ capture=True),
1882++ ])
1883++
1884++ @mock.patch('cloudinit.util.subp')
1885++ def test_rename_all_bounce(self, mock_subp):
1886++ renames = [
1887++ ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'),
1888++ ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'),
1889++ ]
1890++ current_info = {
1891++ 'ens3': {
1892++ 'downable': True,
1893++ 'device_id': '0x3',
1894++ 'driver': 'virtio_net',
1895++ 'mac': '00:11:22:33:44:55',
1896++ 'name': 'ens3',
1897++ 'up': True},
1898++ 'ens5': {
1899++ 'downable': True,
1900++ 'device_id': '0x5',
1901++ 'driver': 'virtio_net',
1902++ 'mac': '00:11:22:33:44:aa',
1903++ 'name': 'ens5',
1904++ 'up': True},
1905++ }
1906++ net._rename_interfaces(renames, current_info=current_info)
1907++ print(mock_subp.call_args_list)
1908++ mock_subp.assert_has_calls([
1909++ mock.call(['ip', 'link', 'set', 'ens3', 'down'], capture=True),
1910++ mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'],
1911++ capture=True),
1912++ mock.call(['ip', 'link', 'set', 'ens5', 'down'], capture=True),
1913++ mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'],
1914++ capture=True),
1915++ mock.call(['ip', 'link', 'set', 'interface0', 'up'], capture=True),
1916++ mock.call(['ip', 'link', 'set', 'interface2', 'up'], capture=True)
1917++ ])
1918++
1919++ @mock.patch('cloudinit.util.subp')
1920++ def test_rename_duplicate_macs(self, mock_subp):
1921++ renames = [
1922++ ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'),
1923++ ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'),
1924++ ]
1925++ current_info = {
1926++ 'eth0': {
1927++ 'downable': True,
1928++ 'device_id': '0x3',
1929++ 'driver': 'hv_netsvc',
1930++ 'mac': '00:11:22:33:44:55',
1931++ 'name': 'eth0',
1932++ 'up': False},
1933++ 'eth1': {
1934++ 'downable': True,
1935++ 'device_id': '0x5',
1936++ 'driver': 'mlx4_core',
1937++ 'mac': '00:11:22:33:44:55',
1938++ 'name': 'eth1',
1939++ 'up': False},
1940++ }
1941++ net._rename_interfaces(renames, current_info=current_info)
1942++ print(mock_subp.call_args_list)
1943++ mock_subp.assert_has_calls([
1944++ mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
1945++ capture=True),
1946++ ])
1947++
1948++ @mock.patch('cloudinit.util.subp')
1949++ def test_rename_duplicate_macs_driver_no_devid(self, mock_subp):
1950++ renames = [
1951++ ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', None),
1952++ ('00:11:22:33:44:55', 'vf1', 'mlx4_core', None),
1953++ ]
1954++ current_info = {
1955++ 'eth0': {
1956++ 'downable': True,
1957++ 'device_id': '0x3',
1958++ 'driver': 'hv_netsvc',
1959++ 'mac': '00:11:22:33:44:55',
1960++ 'name': 'eth0',
1961++ 'up': False},
1962++ 'eth1': {
1963++ 'downable': True,
1964++ 'device_id': '0x5',
1965++ 'driver': 'mlx4_core',
1966++ 'mac': '00:11:22:33:44:55',
1967++ 'name': 'eth1',
1968++ 'up': False},
1969++ }
1970++ net._rename_interfaces(renames, current_info=current_info)
1971++ print(mock_subp.call_args_list)
1972++ mock_subp.assert_has_calls([
1973++ mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
1974++ capture=True),
1975++ ])
1976++
1977++ @mock.patch('cloudinit.util.subp')
1978++ def test_rename_multi_mac_dups(self, mock_subp):
1979++ renames = [
1980++ ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'),
1981++ ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'),
1982++ ('00:11:22:33:44:55', 'vf2', 'mlx4_core', '0x7'),
1983++ ]
1984++ current_info = {
1985++ 'eth0': {
1986++ 'downable': True,
1987++ 'device_id': '0x3',
1988++ 'driver': 'hv_netsvc',
1989++ 'mac': '00:11:22:33:44:55',
1990++ 'name': 'eth0',
1991++ 'up': False},
1992++ 'eth1': {
1993++ 'downable': True,
1994++ 'device_id': '0x5',
1995++ 'driver': 'mlx4_core',
1996++ 'mac': '00:11:22:33:44:55',
1997++ 'name': 'eth1',
1998++ 'up': False},
1999++ 'eth2': {
2000++ 'downable': True,
2001++ 'device_id': '0x7',
2002++ 'driver': 'mlx4_core',
2003++ 'mac': '00:11:22:33:44:55',
2004++ 'name': 'eth2',
2005++ 'up': False},
2006++ }
2007++ net._rename_interfaces(renames, current_info=current_info)
2008++ print(mock_subp.call_args_list)
2009++ mock_subp.assert_has_calls([
2010++ mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
2011++ capture=True),
2012++ mock.call(['ip', 'link', 'set', 'eth2', 'name', 'vf2'],
2013++ capture=True),
2014++ ])
2015++
2016++
2017+ # vi: ts=4 expandtab
2018diff --git a/debian/patches/series b/debian/patches/series
2019index 7669c82..941ab75 100644
2020--- a/debian/patches/series
2021+++ b/debian/patches/series
2022@@ -1,2 +1,7 @@
2023 azure-use-walinux-agent.patch
2024+cpick-5fb49bac-azure-identify-platform-by-well-known-value-in-chassis
2025 ds-identify-behavior-xenial.patch
2026+cpick-003c6678-net-remove-systemd-link-file-writing-from-eni-renderer
2027+cpick-1cd4323b-azure-remove-accidental-duplicate-line-in-merge
2028+cpick-ebc9ecbc-Azure-Add-network-config-Refactor-net-layer-to-handle
2029+cpick-11121fe4-systemd-make-cloud-final.service-run-before-apt-daily

Subscribers

People subscribed via source and target branches