Merge ~smoser/cloud-init:ubuntu/xenial into cloud-init:ubuntu/xenial
- Git
- lp:~smoser/cloud-init
- ubuntu/xenial
- Merge into ubuntu/xenial
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) |
Related bugs: |
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:/
003c6678: net: remove systemd link file writing from eni renderer
https:/
1cd4323b: azure: remove accidental duplicate line in merge.
https:/
ebc9ecbc: Azure: Add network-config, Refactor net layer to handle
https:/
11121fe4: systemd: make cloud-final.service run before apt daily services.
https:/
Server Team CI bot (server-team-bot) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:57d5278faeb
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild:
https:/
Joshua Powers (powersj) wrote : | # |
Ran the built deb using the latest tests from master and it passed:
https:/
+1
Preview Diff
1 | diff --git a/debian/changelog b/debian/changelog |
2 | index 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 | |
26 | diff --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 |
27 | new file mode 100644 |
28 | index 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) |
127 | diff --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 |
128 | new file mode 100644 |
129 | index 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 |
166 | diff --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 |
167 | new file mode 100644 |
168 | index 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] |
194 | diff --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 |
195 | new file mode 100644 |
196 | index 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" |
538 | diff --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 |
539 | new file mode 100644 |
540 | index 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 |
2018 | diff --git a/debian/patches/series b/debian/patches/series |
2019 | index 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 |
FAILED: Continuous integration, rev:64db24de098 3677bea9100055a 9b69384e3bd421 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 19/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 19/rebuild
https:/