Merge ~smoser/cloud-init:fix/1798117-allow-toplevel-network-in-network-config into cloud-init:master

Proposed by Scott Moser
Status: Merged
Approved by: Scott Moser
Approved revision: a1337d0c644e80d40c38d153338082b53de44c63
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~smoser/cloud-init:fix/1798117-allow-toplevel-network-in-network-config
Merge into: cloud-init:master
Diff against target: 262 lines (+97/-35)
2 files modified
cloudinit/sources/DataSourceNoCloud.py (+31/-1)
tests/unittests/test_datasource/test_nocloud.py (+66/-34)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Chad Smith Approve
Review via email: mp+356850@code.launchpad.net

Commit message

NoCloud: Allow top level 'network' key in network-config.

NoCloud's 'network-config' file was originally expected to contain
network configuration without the top level 'network' key. This was
because the file was named 'network-config' so specifying 'network'
seemed redundant.

However, JuJu is currently providing a top level 'network' config when
it tries to disable networking ({"network": {"config": "disabled"}).
Other users have also been surprised/confused by the fact that
a network config in /etc/cloud/cloud.cfg.d/network.cfg differed from
what was expected in 'network-config'.

LP: #1798117

Description of the change

see commit message

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

PASSED: Continuous integration, rev:02d7c7bfdefceb71db86cec0ca09dea49576fc34
https://jenkins.ubuntu.com/server/job/cloud-init-ci/394/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:08621f1188566b888240a408dbe019fec03439ec
https://jenkins.ubuntu.com/server/job/cloud-init-ci/419/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Thanks. LGTM!

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

FAILED: Autolanding.
More details in the following jenkins job:
https://jenkins.ubuntu.com/server/job/cloud-init-autoland-test/101/
Executed test runs:
    FAILED: Checkout

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

PASSED: Continuous integration, rev:a1337d0c644e80d40c38d153338082b53de44c63
https://jenkins.ubuntu.com/server/job/cloud-init-ci/476/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

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

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

marking approved based on c-i now approving.
the fail'd auto-land was a infrastructure failure.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index 9010f06..6860f0c 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -311,6 +311,35 @@ def parse_cmdline_data(ds_id, fill, cmdline=None):
311 return True311 return True
312312
313313
314def _maybe_remove_top_network(cfg):
315 """If network-config contains top level 'network' key, then remove it.
316
317 Some providers of network configuration may provide a top level
318 'network' key (LP: #1798117) even though it is not necessary.
319
320 Be friendly and remove it if it really seems so.
321
322 Return the original value if no change or the updated value if changed."""
323 nullval = object()
324 network_val = cfg.get('network', nullval)
325 if network_val is nullval:
326 return cfg
327 bmsg = 'Top level network key in network-config %s: %s'
328 if not isinstance(network_val, dict):
329 LOG.debug(bmsg, "was not a dict", cfg)
330 return cfg
331 if len(list(cfg.keys())) != 1:
332 LOG.debug(bmsg, "had multiple top level keys", cfg)
333 return cfg
334 if network_val.get('config') == "disabled":
335 LOG.debug(bmsg, "was config/disabled", cfg)
336 elif not all(('config' in network_val, 'version' in network_val)):
337 LOG.debug(bmsg, "but missing 'config' or 'version'", cfg)
338 return cfg
339 LOG.debug(bmsg, "fixed by removing shifting network.", cfg)
340 return network_val
341
342
314def _merge_new_seed(cur, seeded):343def _merge_new_seed(cur, seeded):
315 ret = cur.copy()344 ret = cur.copy()
316345
@@ -320,7 +349,8 @@ def _merge_new_seed(cur, seeded):
320 ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd])349 ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd])
321350
322 if seeded.get('network-config'):351 if seeded.get('network-config'):
323 ret['network-config'] = util.load_yaml(seeded['network-config'])352 ret['network-config'] = _maybe_remove_top_network(
353 util.load_yaml(seeded.get('network-config')))
324354
325 if 'user-data' in seeded:355 if 'user-data' in seeded:
326 ret['user-data'] = seeded['user-data']356 ret['user-data'] = seeded['user-data']
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index b6468b6..3429272 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -1,7 +1,10 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3from cloudinit import helpers3from cloudinit import helpers
4from cloudinit.sources import DataSourceNoCloud4from cloudinit.sources.DataSourceNoCloud import (
5 DataSourceNoCloud as dsNoCloud,
6 _maybe_remove_top_network,
7 parse_cmdline_data)
5from cloudinit import util8from cloudinit import util
6from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack9from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack
710
@@ -40,9 +43,7 @@ class TestNoCloudDataSource(CiTestCase):
40 'datasource': {'NoCloud': {'fs_label': None}}43 'datasource': {'NoCloud': {'fs_label': None}}
41 }44 }
4245
43 ds = DataSourceNoCloud.DataSourceNoCloud46 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
44
45 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
46 ret = dsrc.get_data()47 ret = dsrc.get_data()
47 self.assertEqual(dsrc.userdata_raw, ud)48 self.assertEqual(dsrc.userdata_raw, ud)
48 self.assertEqual(dsrc.metadata, md)49 self.assertEqual(dsrc.metadata, md)
@@ -63,9 +64,7 @@ class TestNoCloudDataSource(CiTestCase):
63 'datasource': {'NoCloud': {'fs_label': None}}64 'datasource': {'NoCloud': {'fs_label': None}}
64 }65 }
6566
66 ds = DataSourceNoCloud.DataSourceNoCloud67 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
67
68 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
69 self.assertTrue(dsrc.get_data())68 self.assertTrue(dsrc.get_data())
70 self.assertEqual(dsrc.platform_type, 'nocloud')69 self.assertEqual(dsrc.platform_type, 'nocloud')
71 self.assertEqual(70 self.assertEqual(
@@ -73,8 +72,6 @@ class TestNoCloudDataSource(CiTestCase):
7372
74 def test_fs_label(self, m_is_lxd):73 def test_fs_label(self, m_is_lxd):
75 # find_devs_with should not be called ff fs_label is None74 # find_devs_with should not be called ff fs_label is None
76 ds = DataSourceNoCloud.DataSourceNoCloud
77
78 class PsuedoException(Exception):75 class PsuedoException(Exception):
79 pass76 pass
8077
@@ -84,12 +81,12 @@ class TestNoCloudDataSource(CiTestCase):
8481
85 # by default, NoCloud should search for filesystems by label82 # by default, NoCloud should search for filesystems by label
86 sys_cfg = {'datasource': {'NoCloud': {}}}83 sys_cfg = {'datasource': {'NoCloud': {}}}
87 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)84 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
88 self.assertRaises(PsuedoException, dsrc.get_data)85 self.assertRaises(PsuedoException, dsrc.get_data)
8986
90 # but disabling searching should just end up with None found87 # but disabling searching should just end up with None found
91 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}88 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
92 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)89 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
93 ret = dsrc.get_data()90 ret = dsrc.get_data()
94 self.assertFalse(ret)91 self.assertFalse(ret)
9592
@@ -97,13 +94,10 @@ class TestNoCloudDataSource(CiTestCase):
97 # no source should be found if no cmdline, config, and fs_label=None94 # no source should be found if no cmdline, config, and fs_label=None
98 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}95 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
9996
100 ds = DataSourceNoCloud.DataSourceNoCloud97 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
101 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
102 self.assertFalse(dsrc.get_data())98 self.assertFalse(dsrc.get_data())
10399
104 def test_seed_in_config(self, m_is_lxd):100 def test_seed_in_config(self, m_is_lxd):
105 ds = DataSourceNoCloud.DataSourceNoCloud
106
107 data = {101 data = {
108 'fs_label': None,102 'fs_label': None,
109 'meta-data': yaml.safe_dump({'instance-id': 'IID'}),103 'meta-data': yaml.safe_dump({'instance-id': 'IID'}),
@@ -111,7 +105,7 @@ class TestNoCloudDataSource(CiTestCase):
111 }105 }
112106
113 sys_cfg = {'datasource': {'NoCloud': data}}107 sys_cfg = {'datasource': {'NoCloud': data}}
114 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)108 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
115 ret = dsrc.get_data()109 ret = dsrc.get_data()
116 self.assertEqual(dsrc.userdata_raw, b"USER_DATA_RAW")110 self.assertEqual(dsrc.userdata_raw, b"USER_DATA_RAW")
117 self.assertEqual(dsrc.metadata.get('instance-id'), 'IID')111 self.assertEqual(dsrc.metadata.get('instance-id'), 'IID')
@@ -130,9 +124,7 @@ class TestNoCloudDataSource(CiTestCase):
130 'datasource': {'NoCloud': {'fs_label': None}}124 'datasource': {'NoCloud': {'fs_label': None}}
131 }125 }
132126
133 ds = DataSourceNoCloud.DataSourceNoCloud127 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
134
135 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
136 ret = dsrc.get_data()128 ret = dsrc.get_data()
137 self.assertEqual(dsrc.userdata_raw, ud)129 self.assertEqual(dsrc.userdata_raw, ud)
138 self.assertEqual(dsrc.metadata, md)130 self.assertEqual(dsrc.metadata, md)
@@ -145,9 +137,7 @@ class TestNoCloudDataSource(CiTestCase):
145137
146 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}138 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
147139
148 ds = DataSourceNoCloud.DataSourceNoCloud140 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
149
150 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
151 ret = dsrc.get_data()141 ret = dsrc.get_data()
152 self.assertEqual(dsrc.userdata_raw, b"ud")142 self.assertEqual(dsrc.userdata_raw, b"ud")
153 self.assertFalse(dsrc.vendordata)143 self.assertFalse(dsrc.vendordata)
@@ -174,9 +164,7 @@ class TestNoCloudDataSource(CiTestCase):
174164
175 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}165 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
176166
177 ds = DataSourceNoCloud.DataSourceNoCloud167 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
178
179 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
180 ret = dsrc.get_data()168 ret = dsrc.get_data()
181 self.assertTrue(ret)169 self.assertTrue(ret)
182 # very simple check just for the strings above170 # very simple check just for the strings above
@@ -195,9 +183,23 @@ class TestNoCloudDataSource(CiTestCase):
195183
196 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}184 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
197185
198 ds = DataSourceNoCloud.DataSourceNoCloud186 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
187 ret = dsrc.get_data()
188 self.assertTrue(ret)
189 self.assertEqual(netconf, dsrc.network_config)
190
191 def test_metadata_network_config_with_toplevel_network(self, m_is_lxd):
192 """network-config may have 'network' top level key."""
193 netconf = {'config': 'disabled'}
194 populate_dir(
195 os.path.join(self.paths.seed_dir, "nocloud"),
196 {'user-data': b"ud",
197 'meta-data': "instance-id: IID\n",
198 'network-config': yaml.dump({'network': netconf}) + "\n"})
199
200 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
199201
200 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)202 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
201 ret = dsrc.get_data()203 ret = dsrc.get_data()
202 self.assertTrue(ret)204 self.assertTrue(ret)
203 self.assertEqual(netconf, dsrc.network_config)205 self.assertEqual(netconf, dsrc.network_config)
@@ -228,9 +230,7 @@ class TestNoCloudDataSource(CiTestCase):
228230
229 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}231 sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
230232
231 ds = DataSourceNoCloud.DataSourceNoCloud233 dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
232
233 dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths)
234 ret = dsrc.get_data()234 ret = dsrc.get_data()
235 self.assertTrue(ret)235 self.assertTrue(ret)
236 self.assertEqual(netconf, dsrc.network_config)236 self.assertEqual(netconf, dsrc.network_config)
@@ -258,8 +258,7 @@ class TestParseCommandLineData(CiTestCase):
258 for (fmt, expected) in pairs:258 for (fmt, expected) in pairs:
259 fill = {}259 fill = {}
260 cmdline = fmt % {'ds_id': ds_id}260 cmdline = fmt % {'ds_id': ds_id}
261 ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill,261 ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline)
262 cmdline=cmdline)
263 self.assertEqual(expected, fill)262 self.assertEqual(expected, fill)
264 self.assertTrue(ret)263 self.assertTrue(ret)
265264
@@ -276,10 +275,43 @@ class TestParseCommandLineData(CiTestCase):
276275
277 for cmdline in cmdlines:276 for cmdline in cmdlines:
278 fill = {}277 fill = {}
279 ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill,278 ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline)
280 cmdline=cmdline)
281 self.assertEqual(fill, {})279 self.assertEqual(fill, {})
282 self.assertFalse(ret)280 self.assertFalse(ret)
283281
284282
283class TestMaybeRemoveToplevelNetwork(CiTestCase):
284 """test _maybe_remove_top_network function."""
285 basecfg = [{'type': 'physical', 'name': 'interface0',
286 'subnets': [{'type': 'dhcp'}]}]
287
288 def test_should_remove_safely(self):
289 mcfg = {'config': self.basecfg, 'version': 1}
290 self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg}))
291
292 def test_no_remove_if_other_keys(self):
293 """should not shift if other keys at top level."""
294 mcfg = {'network': {'config': self.basecfg, 'version': 1},
295 'unknown_keyname': 'keyval'}
296 self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
297
298 def test_no_remove_if_non_dict(self):
299 """should not shift if not a dict."""
300 mcfg = {'network': '"content here'}
301 self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
302
303 def test_no_remove_if_missing_config_or_version(self):
304 """should not shift unless network entry has config and version."""
305 mcfg = {'network': {'config': self.basecfg}}
306 self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
307
308 mcfg = {'network': {'version': 1}}
309 self.assertEqual(mcfg, _maybe_remove_top_network(mcfg))
310
311 def test_remove_with_config_disabled(self):
312 """network/config=disabled should be shifted."""
313 mcfg = {'config': 'disabled'}
314 self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg}))
315
316
285# vi: ts=4 expandtab317# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches