Merge lp:~smoser/cloud-init/trunk.lp1602373 into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
Status: Merged
Merged at revision: 1255
Proposed branch: lp:~smoser/cloud-init/trunk.lp1602373
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 259 lines (+155/-14)
5 files modified
cloudinit/distros/__init__.py (+26/-2)
cloudinit/net/eni.py (+21/-3)
cloudinit/sources/DataSourceConfigDrive.py (+17/-9)
tests/unittests/test_distros/test_netconfig.py (+60/-0)
tests/unittests/test_net.py (+31/-0)
To merge this branch: bzr merge lp:~smoser/cloud-init/trunk.lp1602373
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+300021@code.launchpad.net

Commit message

ConfigDrive: fix writing of 'injected' files and legacy networking

Previous commit inadvertently disabled the consumption of 'injected' files
in configdrive (openstack server boot --file=/target/file=local-file)
unless the datasource was in 'pass' mode. The default mode is 'net' so
that would never happen.

Also here are:
a.) some comments to apply_network_config
b.) add backwards compatibility for distros that do not yet implement
    apply_network_config by converting the network config into ENI format
    and calling apply_network.

    This is required because prior to the previous commit, those distros
    would have had 'apply_network' called with the openstack provided
    ENI file. But after this change they will have apply_network_config
    called by cloudinit's main.

c.) add network_state_to_eni for converting net config to eni
    it supports the not-actually-correct 'hwaddress' field in ENI

To post a comment you must log in.
1247. By Scott Moser

pass the return back up, shorten lines some.

1248. By Scott Moser

merge from trunk

1249. By Scott Moser

add test of apply_network fallback path.

we could do this more simply by mocking fbsd.apply_network
and checking it's inputs. but this pushes it through the whole
path that the other test does.

1250. By Scott Moser

flake8

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cloudinit/distros/__init__.py'
--- cloudinit/distros/__init__.py 2016-06-16 02:50:12 +0000
+++ cloudinit/distros/__init__.py 2016-07-14 18:21:43 +0000
@@ -32,6 +32,8 @@
32from cloudinit import importer32from cloudinit import importer
33from cloudinit import log as logging33from cloudinit import log as logging
34from cloudinit import net34from cloudinit import net
35from cloudinit.net import eni
36from cloudinit.net import network_state
35from cloudinit import ssh_util37from cloudinit import ssh_util
36from cloudinit import type_utils38from cloudinit import type_utils
37from cloudinit import util39from cloudinit import util
@@ -138,9 +140,31 @@
138 return self._bring_up_interfaces(dev_names)140 return self._bring_up_interfaces(dev_names)
139 return False141 return False
140142
143 def _apply_network_from_network_config(self, netconfig, bring_up=True):
144 distro = self.__class__
145 LOG.warn("apply_network_config is not currently implemented "
146 "for distribution '%s'. Attempting to use apply_network",
147 distro)
148 header = '\n'.join([
149 "# Converted from network_config for distro %s" % distro,
150 "# Implmentation of _write_network_config is needed."
151 ])
152 ns = network_state.parse_net_config_data(netconfig)
153 contents = eni.network_state_to_eni(
154 ns, header=header, render_hwaddress=True)
155 return self.apply_network(contents, bring_up=bring_up)
156
141 def apply_network_config(self, netconfig, bring_up=False):157 def apply_network_config(self, netconfig, bring_up=False):
142 # Write it out158 # apply network config netconfig
143 dev_names = self._write_network_config(netconfig)159 # This method is preferred to apply_network which only takes
160 # a much less complete network config format (interfaces(5)).
161 try:
162 dev_names = self._write_network_config(netconfig)
163 except NotImplementedError:
164 # backwards compat until all distros have apply_network_config
165 return self._apply_network_from_network_config(
166 netconfig, bring_up=bring_up)
167
144 # Now try to bring them up168 # Now try to bring them up
145 if bring_up:169 if bring_up:
146 return self._bring_up_interfaces(dev_names)170 return self._bring_up_interfaces(dev_names)
147171
=== modified file 'cloudinit/net/eni.py'
--- cloudinit/net/eni.py 2016-07-13 22:10:54 +0000
+++ cloudinit/net/eni.py 2016-07-14 18:21:43 +0000
@@ -352,7 +352,7 @@
352 content += down + route_line + eol352 content += down + route_line + eol
353 return content353 return content
354354
355 def _render_interfaces(self, network_state):355 def _render_interfaces(self, network_state, render_hwaddress=False):
356 '''Given state, emit etc/network/interfaces content.'''356 '''Given state, emit etc/network/interfaces content.'''
357357
358 content = ""358 content = ""
@@ -397,6 +397,8 @@
397 iface['mode'] = 'dhcp'397 iface['mode'] = 'dhcp'
398398
399 content += _iface_start_entry(iface, index)399 content += _iface_start_entry(iface, index)
400 if render_hwaddress and iface.get('mac_address'):
401 content += " hwaddress %s" % iface['mac_address']
400 content += _iface_add_subnet(iface, subnet)402 content += _iface_add_subnet(iface, subnet)
401 content += _iface_add_attrs(iface)403 content += _iface_add_attrs(iface)
402 for route in subnet.get('routes', []):404 for route in subnet.get('routes', []):
@@ -411,8 +413,6 @@
411 for route in network_state.iter_routes():413 for route in network_state.iter_routes():
412 content += self._render_route(route)414 content += self._render_route(route)
413415
414 # global replacements until v2 format
415 content = content.replace('mac_address', 'hwaddress')
416 return content416 return content
417417
418 def render_network_state(self, target, network_state):418 def render_network_state(self, target, network_state):
@@ -448,3 +448,21 @@
448 ""448 ""
449 ])449 ])
450 util.write_file(fname, content)450 util.write_file(fname, content)
451
452
453def network_state_to_eni(network_state, header=None, render_hwaddress=False):
454 # render the provided network state, return a string of equivalent eni
455 eni_path = 'etc/network/interfaces'
456 renderer = Renderer({
457 'eni_path': eni_path,
458 'eni_header': header,
459 'links_path_prefix': None,
460 'netrules_path': None,
461 })
462 if not header:
463 header = ""
464 if not header.endswith("\n"):
465 header += "\n"
466 contents = renderer._render_interfaces(
467 network_state, render_hwaddress=render_hwaddress)
468 return header + contents
451469
=== modified file 'cloudinit/sources/DataSourceConfigDrive.py'
--- cloudinit/sources/DataSourceConfigDrive.py 2016-06-10 21:16:51 +0000
+++ cloudinit/sources/DataSourceConfigDrive.py 2016-07-14 18:21:43 +0000
@@ -107,12 +107,19 @@
107 if self.dsmode == sources.DSMODE_DISABLED:107 if self.dsmode == sources.DSMODE_DISABLED:
108 return False108 return False
109109
110 # This is legacy and sneaky. If dsmode is 'pass' then write
111 # 'injected files' and apply legacy ENI network format.
112 prev_iid = get_previous_iid(self.paths)110 prev_iid = get_previous_iid(self.paths)
113 cur_iid = md['instance-id']111 cur_iid = md['instance-id']
114 if prev_iid != cur_iid and self.dsmode == sources.DSMODE_PASS:112 if prev_iid != cur_iid:
115 on_first_boot(results, distro=self.distro)113 # better would be to handle this centrally, allowing
114 # the datasource to do something on new instance id
115 # note, networking is only rendered here if dsmode is DSMODE_PASS
116 # which means "DISABLED, but render files and networking"
117 on_first_boot(results, distro=self.distro,
118 network=self.dsmode == sources.DSMODE_PASS)
119
120 # This is legacy and sneaky. If dsmode is 'pass' then do not claim
121 # the datasource was used, even though we did run on_first_boot above.
122 if self.dsmode == sources.DSMODE_PASS:
116 LOG.debug("%s: not claiming datasource, dsmode=%s", self,123 LOG.debug("%s: not claiming datasource, dsmode=%s", self,
117 self.dsmode)124 self.dsmode)
118 return False125 return False
@@ -184,15 +191,16 @@
184 return None191 return None
185192
186193
187def on_first_boot(data, distro=None):194def on_first_boot(data, distro=None, network=True):
188 """Performs any first-boot actions using data read from a config-drive."""195 """Performs any first-boot actions using data read from a config-drive."""
189 if not isinstance(data, dict):196 if not isinstance(data, dict):
190 raise TypeError("Config-drive data expected to be a dict; not %s"197 raise TypeError("Config-drive data expected to be a dict; not %s"
191 % (type(data)))198 % (type(data)))
192 net_conf = data.get("network_config", '')199 if network:
193 if net_conf and distro:200 net_conf = data.get("network_config", '')
194 LOG.warn("Updating network interfaces from config drive")201 if net_conf and distro:
195 distro.apply_network(net_conf)202 LOG.warn("Updating network interfaces from config drive")
203 distro.apply_network(net_conf)
196 write_injected_files(data.get('files'))204 write_injected_files(data.get('files'))
197205
198206
199207
=== modified file 'tests/unittests/test_distros/test_netconfig.py'
--- tests/unittests/test_distros/test_netconfig.py 2016-05-12 20:43:11 +0000
+++ tests/unittests/test_distros/test_netconfig.py 2016-07-14 18:21:43 +0000
@@ -319,3 +319,63 @@
319'''319'''
320 self.assertCfgEquals(expected_buf, str(write_buf))320 self.assertCfgEquals(expected_buf, str(write_buf))
321 self.assertEqual(write_buf.mode, 0o644)321 self.assertEqual(write_buf.mode, 0o644)
322
323 def test_apply_network_config_fallback(self):
324 fbsd_distro = self._get_distro('freebsd')
325
326 # a weak attempt to verify that we don't have an implementation
327 # of _write_network_config or apply_network_config in fbsd now,
328 # which would make this test not actually test the fallback.
329 self.assertRaises(
330 NotImplementedError, fbsd_distro._write_network_config,
331 BASE_NET_CFG)
332
333 # now run
334 mynetcfg = {
335 'config': [{"type": "physical", "name": "eth0",
336 "mac_address": "c0:d6:9f:2c:e8:80",
337 "subnets": [{"type": "dhcp"}]}],
338 'version': 1}
339
340 write_bufs = {}
341 read_bufs = {
342 '/etc/rc.conf': '',
343 '/etc/resolv.conf': '',
344 }
345
346 def replace_write(filename, content, mode=0o644, omode="wb"):
347 buf = WriteBuffer()
348 buf.mode = mode
349 buf.omode = omode
350 buf.write(content)
351 write_bufs[filename] = buf
352
353 def replace_read(fname, read_cb=None, quiet=False):
354 if fname not in read_bufs:
355 if fname in write_bufs:
356 return str(write_bufs[fname])
357 raise IOError("%s not found" % fname)
358 else:
359 if fname in write_bufs:
360 return str(write_bufs[fname])
361 return read_bufs[fname]
362
363 with ExitStack() as mocks:
364 mocks.enter_context(
365 mock.patch.object(util, 'subp', return_value=('vtnet0', '')))
366 mocks.enter_context(
367 mock.patch.object(os.path, 'exists', return_value=False))
368 mocks.enter_context(
369 mock.patch.object(util, 'write_file', replace_write))
370 mocks.enter_context(
371 mock.patch.object(util, 'load_file', replace_read))
372
373 fbsd_distro.apply_network_config(mynetcfg, bring_up=False)
374
375 self.assertIn('/etc/rc.conf', write_bufs)
376 write_buf = write_bufs['/etc/rc.conf']
377 expected_buf = '''
378ifconfig_vtnet0="DHCP"
379'''
380 self.assertCfgEquals(expected_buf, str(write_buf))
381 self.assertEqual(write_buf.mode, 0o644)
322382
=== modified file 'tests/unittests/test_net.py'
--- tests/unittests/test_net.py 2016-06-15 23:11:24 +0000
+++ tests/unittests/test_net.py 2016-07-14 18:21:43 +0000
@@ -269,6 +269,37 @@
269 self.assertEqual(expected.lstrip(), contents.lstrip())269 self.assertEqual(expected.lstrip(), contents.lstrip())
270270
271271
272class TestEniNetworkStateToEni(TestCase):
273 mycfg = {
274 'config': [{"type": "physical", "name": "eth0",
275 "mac_address": "c0:d6:9f:2c:e8:80",
276 "subnets": [{"type": "dhcp"}]}],
277 'version': 1}
278 my_mac = 'c0:d6:9f:2c:e8:80'
279
280 def test_no_header(self):
281 rendered = eni.network_state_to_eni(
282 network_state=network_state.parse_net_config_data(self.mycfg),
283 render_hwaddress=True)
284 self.assertIn(self.my_mac, rendered)
285 self.assertIn("hwaddress", rendered)
286
287 def test_with_header(self):
288 header = "# hello world\n"
289 rendered = eni.network_state_to_eni(
290 network_state=network_state.parse_net_config_data(self.mycfg),
291 header=header, render_hwaddress=True)
292 self.assertIn(header, rendered)
293 self.assertIn(self.my_mac, rendered)
294
295 def test_no_hwaddress(self):
296 rendered = eni.network_state_to_eni(
297 network_state=network_state.parse_net_config_data(self.mycfg),
298 render_hwaddress=False)
299 self.assertNotIn(self.my_mac, rendered)
300 self.assertNotIn("hwaddress", rendered)
301
302
272class TestCmdlineConfigParsing(TestCase):303class TestCmdlineConfigParsing(TestCase):
273 simple_cfg = {304 simple_cfg = {
274 'config': [{"type": "physical", "name": "eth0",305 'config': [{"type": "physical", "name": "eth0",