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

Proposed by Scott Moser on 2016-07-14
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 2016-07-14 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 on 2016-07-14

pass the return back up, shorten lines some.

1248. By Scott Moser on 2016-07-14

merge from trunk

1249. By Scott Moser on 2016-07-14

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 on 2016-07-14

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",