Merge lp:~smoser/cloud-init/trunk.net-improve-lo-dns into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser on 2016-06-21
Status: Merged
Merged at revision: 1256
Proposed branch: lp:~smoser/cloud-init/trunk.net-improve-lo-dns
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 650 lines (+441/-66)
3 files modified
cloudinit/net/eni.py (+102/-66)
tests/unittests/helpers.py (+12/-0)
tests/unittests/test_net.py (+327/-0)
To merge this branch: bzr merge lp:~smoser/cloud-init/trunk.net-improve-lo-dns
Reviewer Review Type Date Requested Status
cloud-init commiters 2016-06-21 Pending
Review via email: mp+298035@code.launchpad.net

Commit Message

improvements to eni rendering

Some improvements here, and some bug fixes.
 - bring curtin revno 394's change to support post-up for interface aliases.
 - sort attributes per interface for nicer order and consistent rendering
 - use arrays for each 'section' rather than content += . This allows better
   separation of the sections and also will perform better as long strings
   with += are slow.
 - improve how 'lo' is handled. If a network state that was being rendered
   had an entry for 'lo', then the rendered ENI would have 2 'lo' sections.
 - no longer skip 'lo' sections when loading an ENI in parse_deb_config
 - fix inet value for subnets, don't add interface attributes to alias
   (LP: #1588547)

Also add some tests of reading yaml and rendering ENI.

To post a comment you must log in.
1261. By Scott Moser on 2016-06-21

fix english in comment

1262. By Scott Moser on 2016-06-21

net: fix inet value for subnets, don't add interface attributes to alias

[copied from curtin revno 390]
Apply two separate fixes for configuring bonding with ip aliases.

Curtin re-used the interface's inet value for each subnet that might
be configured. In the case where the configuration included an ipv4
address after an ipv6 one resulted in emitting 'inet6' for ipv4 address
which is not correct. Resolve this issue by calculating the inet
value independent of the current status of the iface, using the subnet
config instead.

When rendering a network_config which includes ip alias interfaces
do not emit any attributes, like MTU, or bond/bridge options Including
these values is almost always wrong or will result in confusing
behavior on the target system.

1263. By Scott Moser on 2016-06-21

make 2 of 3 tests pass

1264. By Scott Moser on 2016-06-21

fix flake8

1265. By Scott Moser on 2016-06-21

no longer skip 'lo' elements when reading. pass 3rd test.

Scott Moser (smoser) wrote :

passes tox and the 3 added tests now.
The tests are somewhat brittle as they expect exactly rendered ENI content for the provided network yaml.

1266. By Scott Moser on 2016-07-14

merge with trunk

1267. By Scott Moser on 2016-07-14

merge from trunk.lp1602373

this merges in the render_hwaddress support.
newly added tests still run, so hwaddress seems correctly getting in.

1268. By Scott Moser on 2016-07-14

merge from trunk

Ryan Harper (raharper) wrote :

> - use arrays for each 'section' rather than content += . This allows better
> separation of the sections and also will perform better as long strings
> with += are slow.

+1 for better separation. I don't think there is significant difference:

For an 100-line eni (that's rather high for typical usage)

% python3 t.py
Concat: 7.116918564017396
Array : 7.6817819369607605

% python2 t.py
Generating data...
Concat: 8.51635122299
Array : 7.93058490753

For a 10-line eni

% python3 t.py
Generating data...
Concat: 1.0464658139972016
Array : 1.2140009650029242

% python2 t.py
Generating data...
Concat: 0.956037998199
Array : 1.08836984634

% cat t.py
import timeit

concat = """
c=''
for x in range(0, 100):
    c += 'iface eth0 inet static'
"""

append = """
c = []
for x in range(0, 100):
 c.append('iface eth0 inet static')
"""

print('Generating data...')
print("Concat: %s" % timeit.timeit(concat))
print("Array : %s" % timeit.timeit(append))

Ryan Harper (raharper) wrote :

Looks good. Could we add unittests for distro using apply_network, including one without a _write_network so we can test that fallback logic?

1269. By Scott Moser on 2016-07-14

merge from trunk

Scott Moser (smoser) wrote :

i added a test of the _write_network fallback in the fix for 1602373 (revno 1255 on trunk).
and pulled that in here.
going to merge this in now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/net/eni.py'
2--- cloudinit/net/eni.py 2016-07-14 02:36:23 +0000
3+++ cloudinit/net/eni.py 2016-07-14 18:38:19 +0000
4@@ -12,6 +12,7 @@
5 # You should have received a copy of the GNU General Public License
6 # along with this program. If not, see <http://www.gnu.org/licenses/>.
7
8+import copy
9 import glob
10 import os
11 import re
12@@ -42,7 +43,7 @@
13
14 # TODO: switch valid_map based on mode inet/inet6
15 def _iface_add_subnet(iface, subnet):
16- content = ""
17+ content = []
18 valid_map = [
19 'address',
20 'netmask',
21@@ -61,15 +62,21 @@
22 value = " ".join(value)
23 if '_' in key:
24 key = key.replace('_', '-')
25- content += " {0} {1}\n".format(key, value)
26+ content.append(" {0} {1}".format(key, value))
27
28- return content
29+ return sorted(content)
30
31
32 # TODO: switch to valid_map for attrs
33-
34-def _iface_add_attrs(iface):
35- content = ""
36+def _iface_add_attrs(iface, index):
37+ # If the index is non-zero, this is an alias interface. Alias interfaces
38+ # represent additional interface addresses, and should not have additional
39+ # attributes. (extra attributes here are almost always either incorrect,
40+ # or are applied to the parent interface.) So if this is an alias, stop
41+ # right here.
42+ if index != 0:
43+ return []
44+ content = []
45 ignore_map = [
46 'control',
47 'index',
48@@ -79,19 +86,21 @@
49 'subnets',
50 'type',
51 ]
52+ renames = {'mac_address': 'hwaddress'}
53 if iface['type'] not in ['bond', 'bridge', 'vlan']:
54 ignore_map.append('mac_address')
55
56 for key, value in iface.items():
57- if value and key not in ignore_map:
58- if type(value) == list:
59- value = " ".join(value)
60- content += " {0} {1}\n".format(key, value)
61-
62- return content
63-
64-
65-def _iface_start_entry(iface, index):
66+ if not value or key in ignore_map:
67+ continue
68+ if type(value) == list:
69+ value = " ".join(value)
70+ content.append(" {0} {1}".format(renames.get(key, key), value))
71+
72+ return sorted(content)
73+
74+
75+def _iface_start_entry(iface, index, render_hwaddress=False):
76 fullname = iface['name']
77 if index != 0:
78 fullname += ":%s" % index
79@@ -107,8 +116,13 @@
80 subst = iface.copy()
81 subst.update({'fullname': fullname, 'cverb': cverb})
82
83- return ("{cverb} {fullname}\n"
84- "iface {fullname} {inet} {mode}\n").format(**subst)
85+ lines = [
86+ "{cverb} {fullname}".format(**subst),
87+ "iface {fullname} {inet} {mode}".format(**subst)]
88+ if render_hwaddress and iface.get('mac_address'):
89+ lines.append(" hwaddress {mac_address}".format(**subst))
90+
91+ return lines
92
93
94 def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
95@@ -262,10 +276,6 @@
96 for name, data in ifaces.items():
97 # devname is 'eth0' for name='eth0:1'
98 devname = name.partition(":")[0]
99- if devname == "lo":
100- # currently provding 'lo' in network config results in duplicate
101- # entries. in rendered interfaces file. so skip it.
102- continue
103 if devname not in devs:
104 devs[devname] = {'type': 'physical', 'name': devname,
105 'subnets': []}
106@@ -324,10 +334,10 @@
107 1. http://askubuntu.com/questions/168033/
108 how-to-set-static-routes-in-ubuntu-server
109 """
110- content = ""
111+ content = []
112 up = indent + "post-up route add"
113 down = indent + "pre-down route del"
114- eol = " || true\n"
115+ or_true = " || true"
116 mapping = {
117 'network': '-net',
118 'netmask': 'netmask',
119@@ -336,34 +346,84 @@
120 }
121 if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
122 default_gw = " default gw %s" % route['gateway']
123- content += up + default_gw + eol
124- content += down + default_gw + eol
125+ content.append(up + default_gw + or_true)
126+ content.append(down + default_gw + or_true)
127 elif route['network'] == '::' and route['netmask'] == 0:
128 # ipv6!
129 default_gw = " -A inet6 default gw %s" % route['gateway']
130- content += up + default_gw + eol
131- content += down + default_gw + eol
132+ content.append(up + default_gw + or_true)
133+ content.append(down + default_gw + or_true)
134 else:
135 route_line = ""
136 for k in ['network', 'netmask', 'gateway', 'metric']:
137 if k in route:
138 route_line += " %s %s" % (mapping[k], route[k])
139- content += up + route_line + eol
140- content += down + route_line + eol
141+ content.append(up + route_line + or_true)
142+ content.append(down + route_line + or_true)
143 return content
144
145+ def _render_iface(self, iface, render_hwaddress=False):
146+ sections = []
147+ subnets = iface.get('subnets', {})
148+ if subnets:
149+ for index, subnet in zip(range(0, len(subnets)), subnets):
150+ iface['index'] = index
151+ iface['mode'] = subnet['type']
152+ iface['control'] = subnet.get('control', 'auto')
153+ subnet_inet = 'inet'
154+ if iface['mode'].endswith('6'):
155+ # This is a request for DHCPv6.
156+ subnet_inet += '6'
157+ elif iface['mode'] == 'static' and ":" in subnet['address']:
158+ # This is a static IPv6 address.
159+ subnet_inet += '6'
160+ iface['inet'] = subnet_inet
161+ if iface['mode'].startswith('dhcp'):
162+ iface['mode'] = 'dhcp'
163+
164+ lines = list(
165+ _iface_start_entry(
166+ iface, index, render_hwaddress=render_hwaddress) +
167+ _iface_add_subnet(iface, subnet) +
168+ _iface_add_attrs(iface, index)
169+ )
170+ for route in subnet.get('routes', []):
171+ lines.extend(self._render_route(route, indent=" "))
172+
173+ if len(subnets) > 1 and index == 0:
174+ tmpl = " post-up ifup %s:%s\n"
175+ for i in range(1, len(subnets)):
176+ lines.append(tmpl % (iface['name'], i))
177+
178+ sections.append(lines)
179+ else:
180+ # ifenslave docs say to auto the slave devices
181+ lines = []
182+ if 'bond-master' in iface:
183+ lines.append("auto {name}".format(**iface))
184+ lines.append("iface {name} {inet} {mode}".format(**iface))
185+ lines.extend(_iface_add_attrs(iface, index=0))
186+ sections.append(lines)
187+ return sections
188+
189 def _render_interfaces(self, network_state, render_hwaddress=False):
190 '''Given state, emit etc/network/interfaces content.'''
191
192- content = ""
193- content += "auto lo\niface lo inet loopback\n"
194+ # handle 'lo' specifically as we need to insert the global dns entries
195+ # there (as that is the only interface that will be always up).
196+ lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet',
197+ 'subnets': [{'type': 'loopback', 'control': 'auto'}]}
198+ for iface in network_state.iter_interfaces():
199+ if iface.get('name') == "lo":
200+ lo = copy.deepcopy(iface)
201
202 nameservers = network_state.dns_nameservers
203 if nameservers:
204- content += " dns-nameservers %s\n" % (" ".join(nameservers))
205+ lo['subnets'][0]["dns_nameservers"] = (" ".join(nameservers))
206+
207 searchdomains = network_state.dns_searchdomains
208 if searchdomains:
209- content += " dns-search %s\n" % (" ".join(searchdomains))
210+ lo['subnets'][0]["dns_search"] = (" ".join(searchdomains))
211
212 ''' Apply a sort order to ensure that we write out
213 the physical interfaces first; this is critical for
214@@ -375,45 +435,21 @@
215 'bridge': 2,
216 'vlan': 3,
217 }
218+
219+ sections = []
220+ sections.extend(self._render_iface(lo))
221 for iface in sorted(network_state.iter_interfaces(),
222 key=lambda k: (order[k['type']], k['name'])):
223
224- if content[-2:] != "\n\n":
225- content += "\n"
226- subnets = iface.get('subnets', {})
227- if subnets:
228- for index, subnet in zip(range(0, len(subnets)), subnets):
229- if content[-2:] != "\n\n":
230- content += "\n"
231- iface['index'] = index
232- iface['mode'] = subnet['type']
233- iface['control'] = subnet.get('control', 'auto')
234- if iface['mode'].endswith('6'):
235- iface['inet'] += '6'
236- elif (iface['mode'] == 'static' and
237- ":" in subnet['address']):
238- iface['inet'] += '6'
239- if iface['mode'].startswith('dhcp'):
240- iface['mode'] = 'dhcp'
241-
242- content += _iface_start_entry(iface, index)
243- if render_hwaddress and iface.get('mac_address'):
244- content += " hwaddress %s" % iface['mac_address']
245- content += _iface_add_subnet(iface, subnet)
246- content += _iface_add_attrs(iface)
247- for route in subnet.get('routes', []):
248- content += self._render_route(route, indent=" ")
249- else:
250- # ifenslave docs say to auto the slave devices
251- if 'bond-master' in iface:
252- content += "auto {name}\n".format(**iface)
253- content += "iface {name} {inet} {mode}\n".format(**iface)
254- content += _iface_add_attrs(iface)
255+ if iface.get('name') == "lo":
256+ continue
257+ sections.extend(
258+ self._render_iface(iface, render_hwaddress=render_hwaddress))
259
260 for route in network_state.iter_routes():
261- content += self._render_route(route)
262+ sections.append(self._render_route(route))
263
264- return content
265+ return '\n\n'.join(['\n'.join(s) for s in sections]) + "\n"
266
267 def render_network_state(self, target, network_state):
268 fpeni = os.path.join(target, self.eni_path)
269
270=== modified file 'tests/unittests/helpers.py'
271--- tests/unittests/helpers.py 2016-06-10 21:22:17 +0000
272+++ tests/unittests/helpers.py 2016-07-14 18:38:19 +0000
273@@ -264,6 +264,18 @@
274 fp.close()
275
276
277+def dir2dict(startdir, prefix=None):
278+ flist = {}
279+ if prefix is None:
280+ prefix = startdir
281+ for root, dirs, files in os.walk(startdir):
282+ for fname in files:
283+ fpath = os.path.join(root, fname)
284+ key = fpath[len(prefix):]
285+ flist[key] = util.load_file(fpath)
286+ return flist
287+
288+
289 try:
290 skipIf = unittest.skipIf
291 except AttributeError:
292
293=== modified file 'tests/unittests/test_net.py'
294--- tests/unittests/test_net.py 2016-07-14 02:03:42 +0000
295+++ tests/unittests/test_net.py 2016-07-14 18:38:19 +0000
296@@ -6,6 +6,7 @@
297 from cloudinit.sources.helpers import openstack
298 from cloudinit import util
299
300+from .helpers import dir2dict
301 from .helpers import mock
302 from .helpers import TestCase
303
304@@ -17,6 +18,8 @@
305 import os
306 import shutil
307 import tempfile
308+import textwrap
309+import yaml
310
311 DHCP_CONTENT_1 = """
312 DEVICE='eth0'
313@@ -141,6 +144,283 @@
314 }
315 ]
316
317+EXAMPLE_ENI = """
318+auto lo
319+iface lo inet loopback
320+ dns-nameservers 10.0.0.1
321+ dns-search foo.com
322+
323+auto eth0
324+iface eth0 inet static
325+ address 1.2.3.12
326+ netmask 255.255.255.248
327+ broadcast 1.2.3.15
328+ gateway 1.2.3.9
329+ dns-nameservers 69.9.160.191 69.9.191.4
330+auto eth1
331+iface eth1 inet static
332+ address 10.248.2.4
333+ netmask 255.255.255.248
334+ broadcast 10.248.2.7
335+"""
336+
337+RENDERED_ENI = """
338+auto lo
339+iface lo inet loopback
340+ dns-nameservers 10.0.0.1
341+ dns-search foo.com
342+
343+auto eth0
344+iface eth0 inet static
345+ address 1.2.3.12
346+ broadcast 1.2.3.15
347+ dns-nameservers 69.9.160.191 69.9.191.4
348+ gateway 1.2.3.9
349+ netmask 255.255.255.248
350+
351+auto eth1
352+iface eth1 inet static
353+ address 10.248.2.4
354+ broadcast 10.248.2.7
355+ netmask 255.255.255.248
356+""".lstrip()
357+
358+NETWORK_CONFIGS = {
359+ 'small': {
360+ 'expected_eni': textwrap.dedent("""\
361+ auto lo
362+ iface lo inet loopback
363+ dns-nameservers 1.2.3.4 5.6.7.8
364+ dns-search wark.maas
365+
366+ iface eth1 inet manual
367+
368+ auto eth99
369+ iface eth99 inet dhcp
370+ post-up ifup eth99:1
371+
372+
373+ auto eth99:1
374+ iface eth99:1 inet static
375+ address 192.168.21.3/24
376+ dns-nameservers 8.8.8.8 8.8.4.4
377+ dns-search barley.maas sach.maas
378+ post-up route add default gw 65.61.151.37 || true
379+ pre-down route del default gw 65.61.151.37 || true
380+ """).rstrip(' '),
381+ 'yaml': textwrap.dedent("""
382+ version: 1
383+ config:
384+ # Physical interfaces.
385+ - type: physical
386+ name: eth99
387+ mac_address: "c0:d6:9f:2c:e8:80"
388+ subnets:
389+ - type: dhcp4
390+ - type: static
391+ address: 192.168.21.3/24
392+ dns_nameservers:
393+ - 8.8.8.8
394+ - 8.8.4.4
395+ dns_search: barley.maas sach.maas
396+ routes:
397+ - gateway: 65.61.151.37
398+ netmask: 0.0.0.0
399+ network: 0.0.0.0
400+ metric: 2
401+ - type: physical
402+ name: eth1
403+ mac_address: "cf:d6:af:48:e8:80"
404+ - type: nameserver
405+ address:
406+ - 1.2.3.4
407+ - 5.6.7.8
408+ search:
409+ - wark.maas
410+ """),
411+ },
412+ 'all': {
413+ 'expected_eni': ("""\
414+auto lo
415+iface lo inet loopback
416+ dns-nameservers 8.8.8.8 4.4.4.4 8.8.4.4
417+ dns-search barley.maas wark.maas foobar.maas
418+
419+iface eth0 inet manual
420+
421+auto eth1
422+iface eth1 inet manual
423+ bond-master bond0
424+ bond-mode active-backup
425+
426+auto eth2
427+iface eth2 inet manual
428+ bond-master bond0
429+ bond-mode active-backup
430+
431+iface eth3 inet manual
432+
433+iface eth4 inet manual
434+
435+# control-manual eth5
436+iface eth5 inet dhcp
437+
438+auto bond0
439+iface bond0 inet6 dhcp
440+ bond-mode active-backup
441+ bond-slaves none
442+ hwaddress aa:bb:cc:dd:ee:ff
443+
444+auto br0
445+iface br0 inet static
446+ address 192.168.14.2/24
447+ bridge_ports eth3 eth4
448+ bridge_stp off
449+ post-up ifup br0:1
450+
451+
452+auto br0:1
453+iface br0:1 inet6 static
454+ address 2001:1::1/64
455+
456+auto bond0.200
457+iface bond0.200 inet dhcp
458+ vlan-raw-device bond0
459+ vlan_id 200
460+
461+auto eth0.101
462+iface eth0.101 inet static
463+ address 192.168.0.2/24
464+ dns-nameservers 192.168.0.10 10.23.23.134
465+ dns-search barley.maas sacchromyces.maas brettanomyces.maas
466+ gateway 192.168.0.1
467+ mtu 1500
468+ vlan-raw-device eth0
469+ vlan_id 101
470+ post-up ifup eth0.101:1
471+
472+
473+auto eth0.101:1
474+iface eth0.101:1 inet static
475+ address 192.168.2.10/24
476+
477+post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
478+pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
479+"""),
480+ 'yaml': textwrap.dedent("""
481+ version: 1
482+ config:
483+ # Physical interfaces.
484+ - type: physical
485+ name: eth0
486+ mac_address: "c0:d6:9f:2c:e8:80"
487+ - type: physical
488+ name: eth1
489+ mac_address: "aa:d6:9f:2c:e8:80"
490+ - type: physical
491+ name: eth2
492+ mac_address: "c0:bb:9f:2c:e8:80"
493+ - type: physical
494+ name: eth3
495+ mac_address: "66:bb:9f:2c:e8:80"
496+ - type: physical
497+ name: eth4
498+ mac_address: "98:bb:9f:2c:e8:80"
499+ # specify how ifupdown should treat iface
500+ # control is one of ['auto', 'hotplug', 'manual']
501+ # with manual meaning ifup/ifdown should not affect the iface
502+ # useful for things like iscsi root + dhcp
503+ - type: physical
504+ name: eth5
505+ mac_address: "98:bb:9f:2c:e8:8a"
506+ subnets:
507+ - type: dhcp
508+ control: manual
509+ # VLAN interface.
510+ - type: vlan
511+ name: eth0.101
512+ vlan_link: eth0
513+ vlan_id: 101
514+ mtu: 1500
515+ subnets:
516+ - type: static
517+ address: 192.168.0.2/24
518+ gateway: 192.168.0.1
519+ dns_nameservers:
520+ - 192.168.0.10
521+ - 10.23.23.134
522+ dns_search:
523+ - barley.maas
524+ - sacchromyces.maas
525+ - brettanomyces.maas
526+ - type: static
527+ address: 192.168.2.10/24
528+ # Bond.
529+ - type: bond
530+ name: bond0
531+ # if 'mac_address' is omitted, the MAC is taken from
532+ # the first slave.
533+ mac_address: "aa:bb:cc:dd:ee:ff"
534+ bond_interfaces:
535+ - eth1
536+ - eth2
537+ params:
538+ bond-mode: active-backup
539+ subnets:
540+ - type: dhcp6
541+ # A Bond VLAN.
542+ - type: vlan
543+ name: bond0.200
544+ vlan_link: bond0
545+ vlan_id: 200
546+ subnets:
547+ - type: dhcp4
548+ # A bridge.
549+ - type: bridge
550+ name: br0
551+ bridge_interfaces:
552+ - eth3
553+ - eth4
554+ ipv4_conf:
555+ rp_filter: 1
556+ proxy_arp: 0
557+ forwarding: 1
558+ ipv6_conf:
559+ autoconf: 1
560+ disable_ipv6: 1
561+ use_tempaddr: 1
562+ forwarding: 1
563+ # basically anything in /proc/sys/net/ipv6/conf/.../
564+ params:
565+ bridge_stp: 'off'
566+ bridge_fd: 0
567+ bridge_maxwait: 0
568+ subnets:
569+ - type: static
570+ address: 192.168.14.2/24
571+ - type: static
572+ address: 2001:1::1/64 # default to /64
573+ # A global nameserver.
574+ - type: nameserver
575+ address: 8.8.8.8
576+ search: barley.maas
577+ # global nameservers and search in list form
578+ - type: nameserver
579+ address:
580+ - 4.4.4.4
581+ - 8.8.4.4
582+ search:
583+ - wark.maas
584+ - foobar.maas
585+ # A global route.
586+ - type: route
587+ destination: 10.0.0.0/8
588+ gateway: 11.0.0.1
589+ metric: 3
590+ """).lstrip(),
591+ }
592+}
593+
594
595 def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info,
596 mock_sys_dev_path):
597@@ -354,6 +634,53 @@
598 self.assertEqual(found, self.simple_cfg)
599
600
601+class TestEniRoundTrip(TestCase):
602+ def setUp(self):
603+ super(TestCase, self).setUp()
604+ self.tmp_dir = tempfile.mkdtemp()
605+ self.addCleanup(shutil.rmtree, self.tmp_dir)
606+
607+ def _render_and_read(self, network_config=None, state=None, eni_path=None,
608+ links_prefix=None, netrules_path=None):
609+ if network_config:
610+ ns = network_state.parse_net_config_data(network_config)
611+ elif state:
612+ ns = state
613+ else:
614+ raise ValueError("Expected data or state, got neither")
615+
616+ if eni_path is None:
617+ eni_path = 'etc/network/interfaces'
618+
619+ renderer = eni.Renderer(
620+ config={'eni_path': eni_path, 'links_path_prefix': links_prefix,
621+ 'netrules_path': netrules_path})
622+
623+ renderer.render_network_state(self.tmp_dir, ns)
624+ return dir2dict(self.tmp_dir)
625+
626+ def testsimple_convert_and_render(self):
627+ network_config = eni.convert_eni_data(EXAMPLE_ENI)
628+ files = self._render_and_read(network_config=network_config)
629+ self.assertEqual(
630+ RENDERED_ENI.splitlines(),
631+ files['/etc/network/interfaces'].splitlines())
632+
633+ def testsimple_render_all(self):
634+ entry = NETWORK_CONFIGS['all']
635+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
636+ self.assertEqual(
637+ entry['expected_eni'].splitlines(),
638+ files['/etc/network/interfaces'].splitlines())
639+
640+ def testsimple_render_small(self):
641+ entry = NETWORK_CONFIGS['small']
642+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
643+ self.assertEqual(
644+ entry['expected_eni'].splitlines(),
645+ files['/etc/network/interfaces'].splitlines())
646+
647+
648 def _gzip_data(data):
649 with io.BytesIO() as iobuf:
650 gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf)