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
1=== modified file 'cloudinit/distros/__init__.py'
2--- cloudinit/distros/__init__.py 2016-06-16 02:50:12 +0000
3+++ cloudinit/distros/__init__.py 2016-07-14 18:21:43 +0000
4@@ -32,6 +32,8 @@
5 from cloudinit import importer
6 from cloudinit import log as logging
7 from cloudinit import net
8+from cloudinit.net import eni
9+from cloudinit.net import network_state
10 from cloudinit import ssh_util
11 from cloudinit import type_utils
12 from cloudinit import util
13@@ -138,9 +140,31 @@
14 return self._bring_up_interfaces(dev_names)
15 return False
16
17+ def _apply_network_from_network_config(self, netconfig, bring_up=True):
18+ distro = self.__class__
19+ LOG.warn("apply_network_config is not currently implemented "
20+ "for distribution '%s'. Attempting to use apply_network",
21+ distro)
22+ header = '\n'.join([
23+ "# Converted from network_config for distro %s" % distro,
24+ "# Implmentation of _write_network_config is needed."
25+ ])
26+ ns = network_state.parse_net_config_data(netconfig)
27+ contents = eni.network_state_to_eni(
28+ ns, header=header, render_hwaddress=True)
29+ return self.apply_network(contents, bring_up=bring_up)
30+
31 def apply_network_config(self, netconfig, bring_up=False):
32- # Write it out
33- dev_names = self._write_network_config(netconfig)
34+ # apply network config netconfig
35+ # This method is preferred to apply_network which only takes
36+ # a much less complete network config format (interfaces(5)).
37+ try:
38+ dev_names = self._write_network_config(netconfig)
39+ except NotImplementedError:
40+ # backwards compat until all distros have apply_network_config
41+ return self._apply_network_from_network_config(
42+ netconfig, bring_up=bring_up)
43+
44 # Now try to bring them up
45 if bring_up:
46 return self._bring_up_interfaces(dev_names)
47
48=== modified file 'cloudinit/net/eni.py'
49--- cloudinit/net/eni.py 2016-07-13 22:10:54 +0000
50+++ cloudinit/net/eni.py 2016-07-14 18:21:43 +0000
51@@ -352,7 +352,7 @@
52 content += down + route_line + eol
53 return content
54
55- def _render_interfaces(self, network_state):
56+ def _render_interfaces(self, network_state, render_hwaddress=False):
57 '''Given state, emit etc/network/interfaces content.'''
58
59 content = ""
60@@ -397,6 +397,8 @@
61 iface['mode'] = 'dhcp'
62
63 content += _iface_start_entry(iface, index)
64+ if render_hwaddress and iface.get('mac_address'):
65+ content += " hwaddress %s" % iface['mac_address']
66 content += _iface_add_subnet(iface, subnet)
67 content += _iface_add_attrs(iface)
68 for route in subnet.get('routes', []):
69@@ -411,8 +413,6 @@
70 for route in network_state.iter_routes():
71 content += self._render_route(route)
72
73- # global replacements until v2 format
74- content = content.replace('mac_address', 'hwaddress')
75 return content
76
77 def render_network_state(self, target, network_state):
78@@ -448,3 +448,21 @@
79 ""
80 ])
81 util.write_file(fname, content)
82+
83+
84+def network_state_to_eni(network_state, header=None, render_hwaddress=False):
85+ # render the provided network state, return a string of equivalent eni
86+ eni_path = 'etc/network/interfaces'
87+ renderer = Renderer({
88+ 'eni_path': eni_path,
89+ 'eni_header': header,
90+ 'links_path_prefix': None,
91+ 'netrules_path': None,
92+ })
93+ if not header:
94+ header = ""
95+ if not header.endswith("\n"):
96+ header += "\n"
97+ contents = renderer._render_interfaces(
98+ network_state, render_hwaddress=render_hwaddress)
99+ return header + contents
100
101=== modified file 'cloudinit/sources/DataSourceConfigDrive.py'
102--- cloudinit/sources/DataSourceConfigDrive.py 2016-06-10 21:16:51 +0000
103+++ cloudinit/sources/DataSourceConfigDrive.py 2016-07-14 18:21:43 +0000
104@@ -107,12 +107,19 @@
105 if self.dsmode == sources.DSMODE_DISABLED:
106 return False
107
108- # This is legacy and sneaky. If dsmode is 'pass' then write
109- # 'injected files' and apply legacy ENI network format.
110 prev_iid = get_previous_iid(self.paths)
111 cur_iid = md['instance-id']
112- if prev_iid != cur_iid and self.dsmode == sources.DSMODE_PASS:
113- on_first_boot(results, distro=self.distro)
114+ if prev_iid != cur_iid:
115+ # better would be to handle this centrally, allowing
116+ # the datasource to do something on new instance id
117+ # note, networking is only rendered here if dsmode is DSMODE_PASS
118+ # which means "DISABLED, but render files and networking"
119+ on_first_boot(results, distro=self.distro,
120+ network=self.dsmode == sources.DSMODE_PASS)
121+
122+ # This is legacy and sneaky. If dsmode is 'pass' then do not claim
123+ # the datasource was used, even though we did run on_first_boot above.
124+ if self.dsmode == sources.DSMODE_PASS:
125 LOG.debug("%s: not claiming datasource, dsmode=%s", self,
126 self.dsmode)
127 return False
128@@ -184,15 +191,16 @@
129 return None
130
131
132-def on_first_boot(data, distro=None):
133+def on_first_boot(data, distro=None, network=True):
134 """Performs any first-boot actions using data read from a config-drive."""
135 if not isinstance(data, dict):
136 raise TypeError("Config-drive data expected to be a dict; not %s"
137 % (type(data)))
138- net_conf = data.get("network_config", '')
139- if net_conf and distro:
140- LOG.warn("Updating network interfaces from config drive")
141- distro.apply_network(net_conf)
142+ if network:
143+ net_conf = data.get("network_config", '')
144+ if net_conf and distro:
145+ LOG.warn("Updating network interfaces from config drive")
146+ distro.apply_network(net_conf)
147 write_injected_files(data.get('files'))
148
149
150
151=== modified file 'tests/unittests/test_distros/test_netconfig.py'
152--- tests/unittests/test_distros/test_netconfig.py 2016-05-12 20:43:11 +0000
153+++ tests/unittests/test_distros/test_netconfig.py 2016-07-14 18:21:43 +0000
154@@ -319,3 +319,63 @@
155 '''
156 self.assertCfgEquals(expected_buf, str(write_buf))
157 self.assertEqual(write_buf.mode, 0o644)
158+
159+ def test_apply_network_config_fallback(self):
160+ fbsd_distro = self._get_distro('freebsd')
161+
162+ # a weak attempt to verify that we don't have an implementation
163+ # of _write_network_config or apply_network_config in fbsd now,
164+ # which would make this test not actually test the fallback.
165+ self.assertRaises(
166+ NotImplementedError, fbsd_distro._write_network_config,
167+ BASE_NET_CFG)
168+
169+ # now run
170+ mynetcfg = {
171+ 'config': [{"type": "physical", "name": "eth0",
172+ "mac_address": "c0:d6:9f:2c:e8:80",
173+ "subnets": [{"type": "dhcp"}]}],
174+ 'version': 1}
175+
176+ write_bufs = {}
177+ read_bufs = {
178+ '/etc/rc.conf': '',
179+ '/etc/resolv.conf': '',
180+ }
181+
182+ def replace_write(filename, content, mode=0o644, omode="wb"):
183+ buf = WriteBuffer()
184+ buf.mode = mode
185+ buf.omode = omode
186+ buf.write(content)
187+ write_bufs[filename] = buf
188+
189+ def replace_read(fname, read_cb=None, quiet=False):
190+ if fname not in read_bufs:
191+ if fname in write_bufs:
192+ return str(write_bufs[fname])
193+ raise IOError("%s not found" % fname)
194+ else:
195+ if fname in write_bufs:
196+ return str(write_bufs[fname])
197+ return read_bufs[fname]
198+
199+ with ExitStack() as mocks:
200+ mocks.enter_context(
201+ mock.patch.object(util, 'subp', return_value=('vtnet0', '')))
202+ mocks.enter_context(
203+ mock.patch.object(os.path, 'exists', return_value=False))
204+ mocks.enter_context(
205+ mock.patch.object(util, 'write_file', replace_write))
206+ mocks.enter_context(
207+ mock.patch.object(util, 'load_file', replace_read))
208+
209+ fbsd_distro.apply_network_config(mynetcfg, bring_up=False)
210+
211+ self.assertIn('/etc/rc.conf', write_bufs)
212+ write_buf = write_bufs['/etc/rc.conf']
213+ expected_buf = '''
214+ifconfig_vtnet0="DHCP"
215+'''
216+ self.assertCfgEquals(expected_buf, str(write_buf))
217+ self.assertEqual(write_buf.mode, 0o644)
218
219=== modified file 'tests/unittests/test_net.py'
220--- tests/unittests/test_net.py 2016-06-15 23:11:24 +0000
221+++ tests/unittests/test_net.py 2016-07-14 18:21:43 +0000
222@@ -269,6 +269,37 @@
223 self.assertEqual(expected.lstrip(), contents.lstrip())
224
225
226+class TestEniNetworkStateToEni(TestCase):
227+ mycfg = {
228+ 'config': [{"type": "physical", "name": "eth0",
229+ "mac_address": "c0:d6:9f:2c:e8:80",
230+ "subnets": [{"type": "dhcp"}]}],
231+ 'version': 1}
232+ my_mac = 'c0:d6:9f:2c:e8:80'
233+
234+ def test_no_header(self):
235+ rendered = eni.network_state_to_eni(
236+ network_state=network_state.parse_net_config_data(self.mycfg),
237+ render_hwaddress=True)
238+ self.assertIn(self.my_mac, rendered)
239+ self.assertIn("hwaddress", rendered)
240+
241+ def test_with_header(self):
242+ header = "# hello world\n"
243+ rendered = eni.network_state_to_eni(
244+ network_state=network_state.parse_net_config_data(self.mycfg),
245+ header=header, render_hwaddress=True)
246+ self.assertIn(header, rendered)
247+ self.assertIn(self.my_mac, rendered)
248+
249+ def test_no_hwaddress(self):
250+ rendered = eni.network_state_to_eni(
251+ network_state=network_state.parse_net_config_data(self.mycfg),
252+ render_hwaddress=False)
253+ self.assertNotIn(self.my_mac, rendered)
254+ self.assertNotIn("hwaddress", rendered)
255+
256+
257 class TestCmdlineConfigParsing(TestCase):
258 simple_cfg = {
259 'config': [{"type": "physical", "name": "eth0",