Merge lp:~harlowja/cloud-init/cloud-init-net-sysconfig into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow on 2016-06-10
Status: Merged
Merged at revision: 1242
Proposed branch: lp:~harlowja/cloud-init/cloud-init-net-sysconfig
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 1119 lines (+731/-103) (has conflicts)
8 files modified
cloudinit/distros/debian.py (+10/-2)
cloudinit/distros/rhel.py (+8/-0)
cloudinit/net/__init__.py (+0/-1)
cloudinit/net/eni.py (+30/-39)
cloudinit/net/network_state.py (+75/-29)
cloudinit/net/renderer.py (+48/-0)
cloudinit/net/sysconfig.py (+400/-0)
tests/unittests/test_net.py (+160/-32)
Text conflict in cloudinit/distros/debian.py
To merge this branch: bzr merge lp:~harlowja/cloud-init/cloud-init-net-sysconfig
Reviewer Review Type Date Requested Status
cloud-init commiters 2016-06-10 Pending
Review via email: mp+297115@code.launchpad.net
To post a comment you must log in.
1235. By Joshua Harlow on 2016-06-10

Add a sysconfig rendering test

1236. By Joshua Harlow on 2016-06-10

Add a bunch more sample tests for sysconfig

1237. By Joshua Harlow on 2016-06-13

Refactor some of sysconfig changes -> network_state module

1238. By Joshua Harlow on 2016-06-14

Make the os samples easier to extend (for new samples)

Ryan Harper (raharper) :
Joshua Harlow (harlowja) :
Scott Moser (smoser) wrote :

remove your print statements.
i guess go ahead and fix the TODO that you added.

running 'tox' fails for me, always good to fix that .

the rest of it looks reasonable.

Scott Moser (smoser) wrote :

links_prefix support would be ncie in the renderer.
for persistent naming with systemd.link
that might be shared with eni

1239. By Joshua Harlow on 2016-06-15

Fixup code review comments

1240. By Joshua Harlow on 2016-06-15

Fix line length issues

Joshua Harlow (harlowja) wrote :

done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/distros/debian.py'
2--- cloudinit/distros/debian.py 2016-06-10 21:40:05 +0000
3+++ cloudinit/distros/debian.py 2016-06-15 23:16:02 +0000
4@@ -57,7 +57,11 @@
5 # should only happen say once per instance...)
6 self._runner = helpers.Runners(paths)
7 self.osfamily = 'debian'
8- self._net_renderer = eni.Renderer()
9+ self._net_renderer = eni.Renderer({
10+ 'eni_path': self.network_conf_fn,
11+ 'links_prefix_path': self.links_prefix,
12+ 'netrules_path': None,
13+ })
14
15 def apply_locale(self, locale, out_fn=None):
16 if not out_fn:
17@@ -81,13 +85,17 @@
18 return ['all']
19
20 def _write_network_config(self, netconfig):
21+<<<<<<< TREE
22 ns = parse_net_config_data(netconfig)
23 self._net_renderer.render_network_state(
24 target="/", network_state=ns,
25 eni=self.network_conf_fn, links_prefix=self.links_prefix,
26 netrules=None)
27+=======
28+ ns = net.parse_net_config_data(netconfig)
29+ self._net_renderer.render_network_state("/", ns)
30+>>>>>>> MERGE-SOURCE
31 _maybe_remove_legacy_eth0()
32-
33 return []
34
35 def _bring_up_interfaces(self, device_names):
36
37=== modified file 'cloudinit/distros/rhel.py'
38--- cloudinit/distros/rhel.py 2015-06-02 20:27:57 +0000
39+++ cloudinit/distros/rhel.py 2016-06-15 23:16:02 +0000
40@@ -23,6 +23,8 @@
41 from cloudinit import distros
42 from cloudinit import helpers
43 from cloudinit import log as logging
44+from cloudinit.net.network_state import parse_net_config_data
45+from cloudinit.net import sysconfig
46 from cloudinit import util
47
48 from cloudinit.distros import net_util
49@@ -59,10 +61,16 @@
50 # should only happen say once per instance...)
51 self._runner = helpers.Runners(paths)
52 self.osfamily = 'redhat'
53+ self._net_renderer = sysconfig.Renderer()
54
55 def install_packages(self, pkglist):
56 self.package_command('install', pkgs=pkglist)
57
58+ def _write_network_config(self, netconfig):
59+ ns = parse_net_config_data(netconfig)
60+ self._net_renderer.render_network_state("/", ns)
61+ return []
62+
63 def _write_network(self, settings):
64 # TODO(harlowja) fix this... since this is the ubuntu format
65 entries = net_util.translate_network(settings)
66
67=== modified file 'cloudinit/net/__init__.py'
68--- cloudinit/net/__init__.py 2016-06-07 01:42:29 +0000
69+++ cloudinit/net/__init__.py 2016-06-15 23:16:02 +0000
70@@ -26,7 +26,6 @@
71 LOG = logging.getLogger(__name__)
72 SYS_CLASS_NET = "/sys/class/net/"
73 DEFAULT_PRIMARY_INTERFACE = 'eth0'
74-LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
75
76
77 def sys_dev_path(devname, path=""):
78
79=== modified file 'cloudinit/net/eni.py'
80--- cloudinit/net/eni.py 2016-06-14 19:18:53 +0000
81+++ cloudinit/net/eni.py 2016-06-15 23:16:02 +0000
82@@ -16,10 +16,9 @@
83 import os
84 import re
85
86-from . import LINKS_FNAME_PREFIX
87 from . import ParserError
88
89-from .udev import generate_udev_rule
90+from . import renderer
91
92 from cloudinit import util
93
94@@ -297,21 +296,17 @@
95 'config': [devs[d] for d in sorted(devs)]}
96
97
98-class Renderer(object):
99+class Renderer(renderer.Renderer):
100 """Renders network information in a /etc/network/interfaces format."""
101
102- def _render_persistent_net(self, network_state):
103- """Given state, emit udev rules to map mac to ifname."""
104- content = ""
105- interfaces = network_state.get('interfaces')
106- for iface in interfaces.values():
107- # for physical interfaces write out a persist net udev rule
108- if iface['type'] == 'physical' and \
109- 'name' in iface and iface.get('mac_address'):
110- content += generate_udev_rule(iface['name'],
111- iface['mac_address'])
112-
113- return content
114+ def __init__(self, config=None):
115+ if not config:
116+ config = {}
117+ self.eni_path = config.get('eni_path', 'etc/network/interfaces')
118+ self.links_path_prefix = config.get(
119+ 'links_path_prefix', 'etc/systemd/network/50-cloud-init-')
120+ self.netrules_path = config.get(
121+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
122
123 def _render_route(self, route, indent=""):
124 """When rendering routes for an iface, in some cases applying a route
125@@ -360,7 +355,15 @@
126 '''Given state, emit etc/network/interfaces content.'''
127
128 content = ""
129- interfaces = network_state.get('interfaces')
130+ content += "auto lo\niface lo inet loopback\n"
131+
132+ nameservers = network_state.dns_nameservers
133+ if nameservers:
134+ content += " dns-nameservers %s\n" % (" ".join(nameservers))
135+ searchdomains = network_state.dns_searchdomains
136+ if searchdomains:
137+ content += " dns-search %s\n" % (" ".join(searchdomains))
138+
139 ''' Apply a sort order to ensure that we write out
140 the physical interfaces first; this is critical for
141 bonding
142@@ -371,12 +374,7 @@
143 'bridge': 2,
144 'vlan': 3,
145 }
146- content += "auto lo\niface lo inet loopback\n"
147- for dnskey, value in network_state.get('dns', {}).items():
148- if len(value):
149- content += " dns-{} {}\n".format(dnskey, " ".join(value))
150-
151- for iface in sorted(interfaces.values(),
152+ for iface in sorted(network_state.iter_interfaces(),
153 key=lambda k: (order[k['type']], k['name'])):
154
155 if content[-2:] != "\n\n":
156@@ -409,40 +407,33 @@
157 content += "iface {name} {inet} {mode}\n".format(**iface)
158 content += _iface_add_attrs(iface)
159
160- for route in network_state.get('routes'):
161+ for route in network_state.iter_routes():
162 content += self._render_route(route)
163
164 # global replacements until v2 format
165 content = content.replace('mac_address', 'hwaddress')
166 return content
167
168- def render_network_state(
169- self, target, network_state, eni="etc/network/interfaces",
170- links_prefix=LINKS_FNAME_PREFIX,
171- netrules='etc/udev/rules.d/70-persistent-net.rules',
172- writer=None):
173-
174- fpeni = os.path.sep.join((target, eni,))
175+ def render_network_state(self, target, network_state):
176+ fpeni = os.path.join(target, self.eni_path)
177 util.ensure_dir(os.path.dirname(fpeni))
178 util.write_file(fpeni, self._render_interfaces(network_state))
179
180- if netrules:
181- netrules = os.path.sep.join((target, netrules,))
182+ if self.netrules_path:
183+ netrules = os.path.join(target, self.netrules_path)
184 util.ensure_dir(os.path.dirname(netrules))
185 util.write_file(netrules,
186 self._render_persistent_net(network_state))
187
188- if links_prefix:
189+ if self.links_path_prefix:
190 self._render_systemd_links(target, network_state,
191- links_prefix=links_prefix)
192+ links_prefix=self.links_path_prefix)
193
194- def _render_systemd_links(self, target, network_state,
195- links_prefix=LINKS_FNAME_PREFIX):
196- fp_prefix = os.path.sep.join((target, links_prefix))
197+ def _render_systemd_links(self, target, network_state, links_prefix):
198+ fp_prefix = os.path.join(target, links_prefix)
199 for f in glob.glob(fp_prefix + "*"):
200 os.unlink(f)
201- interfaces = network_state.get('interfaces')
202- for iface in interfaces.values():
203+ for iface in network_state.iter_interfaces():
204 if (iface['type'] == 'physical' and 'name' in iface and
205 iface.get('mac_address')):
206 fname = fp_prefix + iface['name'] + ".link"
207
208=== modified file 'cloudinit/net/network_state.py'
209--- cloudinit/net/network_state.py 2016-06-07 17:59:27 +0000
210+++ cloudinit/net/network_state.py 2016-06-15 23:16:02 +0000
211@@ -38,10 +38,10 @@
212 """
213 state = None
214 if 'version' in net_config and 'config' in net_config:
215- ns = NetworkState(version=net_config.get('version'),
216- config=net_config.get('config'))
217- ns.parse_config(skip_broken=skip_broken)
218- state = ns.network_state
219+ nsi = NetworkStateInterpreter(version=net_config.get('version'),
220+ config=net_config.get('config'))
221+ nsi.parse_config(skip_broken=skip_broken)
222+ state = nsi.network_state
223 return state
224
225
226@@ -57,11 +57,10 @@
227
228
229 def from_state_file(state_file):
230- network_state = None
231 state = util.read_conf(state_file)
232- network_state = NetworkState()
233- network_state.load(state)
234- return network_state
235+ nsi = NetworkStateInterpreter()
236+ nsi.load(state)
237+ return nsi
238
239
240 def diff_keys(expected, actual):
241@@ -113,8 +112,50 @@
242 parents, dct)
243
244
245+class NetworkState(object):
246+
247+ def __init__(self, network_state, version=NETWORK_STATE_VERSION):
248+ self._network_state = copy.deepcopy(network_state)
249+ self._version = version
250+
251+ @property
252+ def version(self):
253+ return self._version
254+
255+ def iter_routes(self, filter_func=None):
256+ for route in self._network_state.get('routes', []):
257+ if filter_func is not None:
258+ if filter_func(route):
259+ yield route
260+ else:
261+ yield route
262+
263+ @property
264+ def dns_nameservers(self):
265+ try:
266+ return self._network_state['dns']['nameservers']
267+ except KeyError:
268+ return []
269+
270+ @property
271+ def dns_searchdomains(self):
272+ try:
273+ return self._network_state['dns']['search']
274+ except KeyError:
275+ return []
276+
277+ def iter_interfaces(self, filter_func=None):
278+ ifaces = self._network_state.get('interfaces', {})
279+ for iface in six.itervalues(ifaces):
280+ if filter_func is None:
281+ yield iface
282+ else:
283+ if filter_func(iface):
284+ yield iface
285+
286+
287 @six.add_metaclass(CommandHandlerMeta)
288-class NetworkState(object):
289+class NetworkStateInterpreter(object):
290
291 initial_network_state = {
292 'interfaces': {},
293@@ -126,22 +167,27 @@
294 }
295
296 def __init__(self, version=NETWORK_STATE_VERSION, config=None):
297- self.version = version
298- self.config = config
299- self.network_state = copy.deepcopy(self.initial_network_state)
300+ self._version = version
301+ self._config = config
302+ self._network_state = copy.deepcopy(self.initial_network_state)
303+ self._parsed = False
304+
305+ @property
306+ def network_state(self):
307+ return NetworkState(self._network_state, version=self._version)
308
309 def dump(self):
310 state = {
311- 'version': self.version,
312- 'config': self.config,
313- 'network_state': self.network_state,
314+ 'version': self._version,
315+ 'config': self._config,
316+ 'network_state': self._network_state,
317 }
318 return util.yaml_dumps(state)
319
320 def load(self, state):
321 if 'version' not in state:
322 LOG.error('Invalid state, missing version field')
323- raise Exception('Invalid state, missing version field')
324+ raise ValueError('Invalid state, missing version field')
325
326 required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
327 missing_keys = diff_keys(required_keys, state)
328@@ -155,11 +201,11 @@
329 setattr(self, key, state[key])
330
331 def dump_network_state(self):
332- return util.yaml_dumps(self.network_state)
333+ return util.yaml_dumps(self._network_state)
334
335 def parse_config(self, skip_broken=True):
336 # rebuild network state
337- for command in self.config:
338+ for command in self._config:
339 command_type = command['type']
340 try:
341 handler = self.command_handlers[command_type]
342@@ -189,7 +235,7 @@
343 }
344 '''
345
346- interfaces = self.network_state.get('interfaces')
347+ interfaces = self._network_state.get('interfaces', {})
348 iface = interfaces.get(command['name'], {})
349 for param, val in command.get('params', {}).items():
350 iface.update({param: val})
351@@ -215,7 +261,7 @@
352 'gateway': None,
353 'subnets': subnets,
354 })
355- self.network_state['interfaces'].update({command.get('name'): iface})
356+ self._network_state['interfaces'].update({command.get('name'): iface})
357 self.dump_network_state()
358
359 @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
360@@ -228,7 +274,7 @@
361 hwaddress ether BC:76:4E:06:96:B3
362 vlan-raw-device eth0
363 '''
364- interfaces = self.network_state.get('interfaces')
365+ interfaces = self._network_state.get('interfaces', {})
366 self.handle_physical(command)
367 iface = interfaces.get(command.get('name'), {})
368 iface['vlan-raw-device'] = command.get('vlan_link')
369@@ -263,12 +309,12 @@
370 '''
371
372 self.handle_physical(command)
373- interfaces = self.network_state.get('interfaces')
374+ interfaces = self._network_state.get('interfaces')
375 iface = interfaces.get(command.get('name'), {})
376 for param, val in command.get('params').items():
377 iface.update({param: val})
378 iface.update({'bond-slaves': 'none'})
379- self.network_state['interfaces'].update({iface['name']: iface})
380+ self._network_state['interfaces'].update({iface['name']: iface})
381
382 # handle bond slaves
383 for ifname in command.get('bond_interfaces'):
384@@ -280,13 +326,13 @@
385 # inject placeholder
386 self.handle_physical(cmd)
387
388- interfaces = self.network_state.get('interfaces')
389+ interfaces = self._network_state.get('interfaces', {})
390 bond_if = interfaces.get(ifname)
391 bond_if['bond-master'] = command.get('name')
392 # copy in bond config into slave
393 for param, val in command.get('params').items():
394 bond_if.update({param: val})
395- self.network_state['interfaces'].update({ifname: bond_if})
396+ self._network_state['interfaces'].update({ifname: bond_if})
397
398 @ensure_command_keys(['name', 'bridge_interfaces', 'params'])
399 def handle_bridge(self, command):
400@@ -319,7 +365,7 @@
401
402 # find one of the bridge port ifaces to get mac_addr
403 # handle bridge_slaves
404- interfaces = self.network_state.get('interfaces')
405+ interfaces = self._network_state.get('interfaces', {})
406 for ifname in command.get('bridge_interfaces'):
407 if ifname in interfaces:
408 continue
409@@ -330,7 +376,7 @@
410 # inject placeholder
411 self.handle_physical(cmd)
412
413- interfaces = self.network_state.get('interfaces')
414+ interfaces = self._network_state.get('interfaces', {})
415 self.handle_physical(command)
416 iface = interfaces.get(command.get('name'), {})
417 iface['bridge_ports'] = command['bridge_interfaces']
418@@ -341,7 +387,7 @@
419
420 @ensure_command_keys(['address'])
421 def handle_nameserver(self, command):
422- dns = self.network_state.get('dns')
423+ dns = self._network_state.get('dns')
424 if 'address' in command:
425 addrs = command['address']
426 if not type(addrs) == list:
427@@ -357,7 +403,7 @@
428
429 @ensure_command_keys(['destination'])
430 def handle_route(self, command):
431- routes = self.network_state.get('routes')
432+ routes = self._network_state.get('routes', [])
433 network, cidr = command['destination'].split("/")
434 netmask = cidr2mask(int(cidr))
435 route = {
436
437=== added file 'cloudinit/net/renderer.py'
438--- cloudinit/net/renderer.py 1970-01-01 00:00:00 +0000
439+++ cloudinit/net/renderer.py 2016-06-15 23:16:02 +0000
440@@ -0,0 +1,48 @@
441+# Copyright (C) 2013-2014 Canonical Ltd.
442+#
443+# Author: Scott Moser <scott.moser@canonical.com>
444+# Author: Blake Rouse <blake.rouse@canonical.com>
445+#
446+# Curtin is free software: you can redistribute it and/or modify it under
447+# the terms of the GNU Affero General Public License as published by the
448+# Free Software Foundation, either version 3 of the License, or (at your
449+# option) any later version.
450+#
451+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
452+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
453+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
454+# more details.
455+#
456+# You should have received a copy of the GNU Affero General Public License
457+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
458+
459+import six
460+
461+from .udev import generate_udev_rule
462+
463+
464+def filter_by_type(match_type):
465+ return lambda iface: match_type == iface['type']
466+
467+
468+def filter_by_name(match_name):
469+ return lambda iface: match_name == iface['name']
470+
471+
472+filter_by_physical = filter_by_type('physical')
473+
474+
475+class Renderer(object):
476+
477+ @staticmethod
478+ def _render_persistent_net(network_state):
479+ """Given state, emit udev rules to map mac to ifname."""
480+ # TODO(harlowja): this seems shared between eni renderer and
481+ # this, so move it to a shared location.
482+ content = six.StringIO()
483+ for iface in network_state.iter_interfaces(filter_by_physical):
484+ # for physical interfaces write out a persist net udev rule
485+ if 'name' in iface and iface.get('mac_address'):
486+ content.write(generate_udev_rule(iface['name'],
487+ iface['mac_address']))
488+ return content.getvalue()
489
490=== added file 'cloudinit/net/sysconfig.py'
491--- cloudinit/net/sysconfig.py 1970-01-01 00:00:00 +0000
492+++ cloudinit/net/sysconfig.py 2016-06-15 23:16:02 +0000
493@@ -0,0 +1,400 @@
494+# vi: ts=4 expandtab
495+#
496+# This program is free software: you can redistribute it and/or modify
497+# it under the terms of the GNU General Public License version 3, as
498+# published by the Free Software Foundation.
499+#
500+# This program is distributed in the hope that it will be useful,
501+# but WITHOUT ANY WARRANTY; without even the implied warranty of
502+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
503+# GNU General Public License for more details.
504+#
505+# You should have received a copy of the GNU General Public License
506+# along with this program. If not, see <http://www.gnu.org/licenses/>.
507+
508+import os
509+import re
510+
511+import six
512+
513+from cloudinit.distros.parsers import resolv_conf
514+from cloudinit import util
515+
516+from . import renderer
517+
518+
519+def _make_header(sep='#'):
520+ lines = [
521+ "Created by cloud-init on instance boot automatically, do not edit.",
522+ "",
523+ ]
524+ for i in range(0, len(lines)):
525+ if lines[i]:
526+ lines[i] = sep + " " + lines[i]
527+ else:
528+ lines[i] = sep
529+ return "\n".join(lines)
530+
531+
532+def _is_default_route(route):
533+ if route['network'] == '::' and route['netmask'] == 0:
534+ return True
535+ if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
536+ return True
537+ return False
538+
539+
540+def _quote_value(value):
541+ if re.search(r"\s", value):
542+ # This doesn't handle complex cases...
543+ if value.startswith('"') and value.endswith('"'):
544+ return value
545+ else:
546+ return '"%s"' % value
547+ else:
548+ return value
549+
550+
551+class ConfigMap(object):
552+ """Sysconfig like dictionary object."""
553+
554+ # Why does redhat prefer yes/no to true/false??
555+ _bool_map = {
556+ True: 'yes',
557+ False: 'no',
558+ }
559+
560+ def __init__(self):
561+ self._conf = {}
562+
563+ def __setitem__(self, key, value):
564+ self._conf[key] = value
565+
566+ def drop(self, key):
567+ self._conf.pop(key, None)
568+
569+ def __len__(self):
570+ return len(self._conf)
571+
572+ def to_string(self):
573+ buf = six.StringIO()
574+ buf.write(_make_header())
575+ if self._conf:
576+ buf.write("\n")
577+ for key in sorted(self._conf.keys()):
578+ value = self._conf[key]
579+ if isinstance(value, bool):
580+ value = self._bool_map[value]
581+ if not isinstance(value, six.string_types):
582+ value = str(value)
583+ buf.write("%s=%s\n" % (key, _quote_value(value)))
584+ return buf.getvalue()
585+
586+
587+class Route(ConfigMap):
588+ """Represents a route configuration."""
589+
590+ route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
591+
592+ def __init__(self, route_name, base_sysconf_dir):
593+ super(Route, self).__init__()
594+ self.last_idx = 1
595+ self.has_set_default = False
596+ self._route_name = route_name
597+ self._base_sysconf_dir = base_sysconf_dir
598+
599+ def copy(self):
600+ r = Route(self._route_name, self._base_sysconf_dir)
601+ r._conf = self._conf.copy()
602+ r.last_idx = self.last_idx
603+ r.has_set_default = self.has_set_default
604+ return r
605+
606+ @property
607+ def path(self):
608+ return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
609+ 'name': self._route_name})
610+
611+
612+class NetInterface(ConfigMap):
613+ """Represents a sysconfig/networking-script (and its config + children)."""
614+
615+ iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
616+
617+ iface_types = {
618+ 'ethernet': 'Ethernet',
619+ 'bond': 'Bond',
620+ 'bridge': 'Bridge',
621+ }
622+
623+ def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
624+ super(NetInterface, self).__init__()
625+ self.children = []
626+ self.routes = Route(iface_name, base_sysconf_dir)
627+ self._kind = kind
628+ self._iface_name = iface_name
629+ self._conf['DEVICE'] = iface_name
630+ self._conf['TYPE'] = self.iface_types[kind]
631+ self._base_sysconf_dir = base_sysconf_dir
632+
633+ @property
634+ def name(self):
635+ return self._iface_name
636+
637+ @name.setter
638+ def name(self, iface_name):
639+ self._iface_name = iface_name
640+ self._conf['DEVICE'] = iface_name
641+
642+ @property
643+ def kind(self):
644+ return self._kind
645+
646+ @kind.setter
647+ def kind(self, kind):
648+ self._kind = kind
649+ self._conf['TYPE'] = self.iface_types[kind]
650+
651+ @property
652+ def path(self):
653+ return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
654+ 'name': self.name})
655+
656+ def copy(self, copy_children=False, copy_routes=False):
657+ c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
658+ c._conf = self._conf.copy()
659+ if copy_children:
660+ c.children = list(self.children)
661+ if copy_routes:
662+ c.routes = self.routes.copy()
663+ return c
664+
665+
666+class Renderer(renderer.Renderer):
667+ """Renders network information in a /etc/sysconfig format."""
668+
669+ # See: https://access.redhat.com/documentation/en-US/\
670+ # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
671+ # s1-networkscripts-interfaces.html (or other docs for
672+ # details about this)
673+
674+ iface_defaults = tuple([
675+ ('ONBOOT', True),
676+ ('USERCTL', False),
677+ ('NM_CONTROLLED', False),
678+ ('BOOTPROTO', 'none'),
679+ ])
680+
681+ # If these keys exist, then there values will be used to form
682+ # a BONDING_OPTS grouping; otherwise no grouping will be set.
683+ bond_tpl_opts = tuple([
684+ ('bond_mode', "mode=%s"),
685+ ('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
686+ ('bond_miimon', "miimon=%s"),
687+ ])
688+
689+ bridge_opts_keys = tuple([
690+ ('bridge_stp', 'STP'),
691+ ('bridge_ageing', 'AGEING'),
692+ ('bridge_bridgeprio', 'PRIO'),
693+ ])
694+
695+ def __init__(self, config=None):
696+ if not config:
697+ config = {}
698+ self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
699+ self.netrules_path = config.get(
700+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
701+ self.dns_path = config.get('dns_path', 'etc/resolv.conf')
702+
703+ @classmethod
704+ def _render_iface_shared(cls, iface, iface_cfg):
705+ for k, v in cls.iface_defaults:
706+ iface_cfg[k] = v
707+ for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
708+ old_value = iface.get(old_key)
709+ if old_value is not None:
710+ iface_cfg[new_key] = old_value
711+
712+ @classmethod
713+ def _render_subnet(cls, iface_cfg, route_cfg, subnet):
714+ subnet_type = subnet.get('type')
715+ if subnet_type == 'dhcp6':
716+ iface_cfg['DHCPV6C'] = True
717+ iface_cfg['IPV6INIT'] = True
718+ iface_cfg['BOOTPROTO'] = 'dhcp'
719+ elif subnet_type in ['dhcp4', 'dhcp']:
720+ iface_cfg['BOOTPROTO'] = 'dhcp'
721+ elif subnet_type == 'static':
722+ iface_cfg['BOOTPROTO'] = 'static'
723+ if subnet.get('ipv6'):
724+ iface_cfg['IPV6ADDR'] = subnet['address']
725+ iface_cfg['IPV6INIT'] = True
726+ else:
727+ iface_cfg['IPADDR'] = subnet['address']
728+ else:
729+ raise ValueError("Unknown subnet type '%s' found"
730+ " for interface '%s'" % (subnet_type,
731+ iface_cfg.name))
732+ if 'netmask' in subnet:
733+ iface_cfg['NETMASK'] = subnet['netmask']
734+ for route in subnet.get('routes', []):
735+ if _is_default_route(route):
736+ if route_cfg.has_set_default:
737+ raise ValueError("Duplicate declaration of default"
738+ " route found for interface '%s'"
739+ % (iface_cfg.name))
740+ # NOTE(harlowja): ipv6 and ipv4 default gateways
741+ gw_key = 'GATEWAY0'
742+ nm_key = 'NETMASK0'
743+ addr_key = 'ADDRESS0'
744+ # The owning interface provides the default route.
745+ #
746+ # TODO(harlowja): add validation that no other iface has
747+ # also provided the default route?
748+ iface_cfg['DEFROUTE'] = True
749+ if 'gateway' in route:
750+ iface_cfg['GATEWAY'] = route['gateway']
751+ route_cfg.has_set_default = True
752+ else:
753+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
754+ nm_key = 'NETMASK%s' % route_cfg.last_idx
755+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
756+ route_cfg.last_idx += 1
757+ for (old_key, new_key) in [('gateway', gw_key),
758+ ('netmask', nm_key),
759+ ('network', addr_key)]:
760+ if old_key in route:
761+ route_cfg[new_key] = route[old_key]
762+
763+ @classmethod
764+ def _render_bonding_opts(cls, iface_cfg, iface):
765+ bond_opts = []
766+ for (bond_key, value_tpl) in cls.bond_tpl_opts:
767+ # Seems like either dash or underscore is possible?
768+ bond_keys = [bond_key, bond_key.replace("_", "-")]
769+ for bond_key in bond_keys:
770+ if bond_key in iface:
771+ bond_value = iface[bond_key]
772+ if isinstance(bond_value, (tuple, list)):
773+ bond_value = " ".join(bond_value)
774+ bond_opts.append(value_tpl % (bond_value))
775+ break
776+ if bond_opts:
777+ iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
778+
779+ @classmethod
780+ def _render_physical_interfaces(cls, network_state, iface_contents):
781+ physical_filter = renderer.filter_by_physical
782+ for iface in network_state.iter_interfaces(physical_filter):
783+ iface_name = iface['name']
784+ iface_subnets = iface.get("subnets", [])
785+ iface_cfg = iface_contents[iface_name]
786+ route_cfg = iface_cfg.routes
787+ if len(iface_subnets) == 1:
788+ cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
789+ elif len(iface_subnets) > 1:
790+ for i, iface_subnet in enumerate(iface_subnets,
791+ start=len(iface.children)):
792+ iface_sub_cfg = iface_cfg.copy()
793+ iface_sub_cfg.name = "%s:%s" % (iface_name, i)
794+ iface.children.append(iface_sub_cfg)
795+ cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
796+
797+ @classmethod
798+ def _render_bond_interfaces(cls, network_state, iface_contents):
799+ bond_filter = renderer.filter_by_type('bond')
800+ for iface in network_state.iter_interfaces(bond_filter):
801+ iface_name = iface['name']
802+ iface_cfg = iface_contents[iface_name]
803+ cls._render_bonding_opts(iface_cfg, iface)
804+ iface_master_name = iface['bond-master']
805+ iface_cfg['MASTER'] = iface_master_name
806+ iface_cfg['SLAVE'] = True
807+ # Ensure that the master interface (and any of its children)
808+ # are actually marked as being bond types...
809+ master_cfg = iface_contents[iface_master_name]
810+ master_cfgs = [master_cfg]
811+ master_cfgs.extend(master_cfg.children)
812+ for master_cfg in master_cfgs:
813+ master_cfg['BONDING_MASTER'] = True
814+ master_cfg.kind = 'bond'
815+
816+ @staticmethod
817+ def _render_vlan_interfaces(network_state, iface_contents):
818+ vlan_filter = renderer.filter_by_type('vlan')
819+ for iface in network_state.iter_interfaces(vlan_filter):
820+ iface_name = iface['name']
821+ iface_cfg = iface_contents[iface_name]
822+ iface_cfg['VLAN'] = True
823+ iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
824+
825+ @staticmethod
826+ def _render_dns(network_state, existing_dns_path=None):
827+ content = resolv_conf.ResolvConf("")
828+ if existing_dns_path and os.path.isfile(existing_dns_path):
829+ content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
830+ for nameserver in network_state.dns_nameservers:
831+ content.add_nameserver(nameserver)
832+ for searchdomain in network_state.dns_searchdomains:
833+ content.add_search_domain(searchdomain)
834+ return "\n".join([_make_header(';'), str(content)])
835+
836+ @classmethod
837+ def _render_bridge_interfaces(cls, network_state, iface_contents):
838+ bridge_filter = renderer.filter_by_type('bridge')
839+ for iface in network_state.iter_interfaces(bridge_filter):
840+ iface_name = iface['name']
841+ iface_cfg = iface_contents[iface_name]
842+ iface_cfg.kind = 'bridge'
843+ for old_key, new_key in cls.bridge_opts_keys:
844+ if old_key in iface:
845+ iface_cfg[new_key] = iface[old_key]
846+ # Is this the right key to get all the connected interfaces?
847+ for bridged_iface_name in iface.get('bridge_ports', []):
848+ # Ensure all bridged interfaces are correctly tagged
849+ # as being bridged to this interface.
850+ bridged_cfg = iface_contents[bridged_iface_name]
851+ bridged_cfgs = [bridged_cfg]
852+ bridged_cfgs.extend(bridged_cfg.children)
853+ for bridge_cfg in bridged_cfgs:
854+ bridge_cfg['BRIDGE'] = iface_name
855+
856+ @classmethod
857+ def _render_sysconfig(cls, base_sysconf_dir, network_state):
858+ '''Given state, return /etc/sysconfig files + contents'''
859+ iface_contents = {}
860+ for iface in network_state.iter_interfaces():
861+ iface_name = iface['name']
862+ iface_cfg = NetInterface(iface_name, base_sysconf_dir)
863+ cls._render_iface_shared(iface, iface_cfg)
864+ iface_contents[iface_name] = iface_cfg
865+ cls._render_physical_interfaces(network_state, iface_contents)
866+ cls._render_bond_interfaces(network_state, iface_contents)
867+ cls._render_vlan_interfaces(network_state, iface_contents)
868+ cls._render_bridge_interfaces(network_state, iface_contents)
869+ contents = {}
870+ for iface_name, iface_cfg in iface_contents.items():
871+ if iface_cfg or iface_cfg.children:
872+ contents[iface_cfg.path] = iface_cfg.to_string()
873+ for iface_cfg in iface_cfg.children:
874+ if iface_cfg:
875+ contents[iface_cfg.path] = iface_cfg.to_string()
876+ if iface_cfg.routes:
877+ contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
878+ return contents
879+
880+ def render_network_state(self, target, network_state):
881+ base_sysconf_dir = os.path.join(target, self.sysconf_dir)
882+ for path, data in self._render_sysconfig(base_sysconf_dir,
883+ network_state).items():
884+ util.write_file(path, data)
885+ if self.dns_path:
886+ dns_path = os.path.join(target, self.dns_path)
887+ resolv_content = self._render_dns(network_state,
888+ existing_dns_path=dns_path)
889+ util.write_file(dns_path, resolv_content)
890+ if self.netrules_path:
891+ netrules_content = self._render_persistent_net(network_state)
892+ netrules_path = os.path.join(target, self.netrules_path)
893+ util.write_file(netrules_path, netrules_content)
894
895=== modified file 'tests/unittests/test_net.py'
896--- tests/unittests/test_net.py 2016-05-19 22:33:15 +0000
897+++ tests/unittests/test_net.py 2016-06-15 23:16:02 +0000
898@@ -2,6 +2,8 @@
899 from cloudinit.net import cmdline
900 from cloudinit.net import eni
901 from cloudinit.net import network_state
902+from cloudinit.net import sysconfig
903+from cloudinit.sources.helpers import openstack
904 from cloudinit import util
905
906 from .helpers import mock
907@@ -74,6 +76,157 @@
908 'dns_nameservers': ['10.0.1.1']}],
909 }
910
911+# Examples (and expected outputs for various renderers).
912+OS_SAMPLES = [
913+ {
914+ 'in_data': {
915+ "services": [{"type": "dns", "address": "172.19.0.12"}],
916+ "networks": [{
917+ "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
918+ "type": "ipv4", "netmask": "255.255.252.0",
919+ "link": "tap1a81968a-79",
920+ "routes": [{
921+ "netmask": "0.0.0.0",
922+ "network": "0.0.0.0",
923+ "gateway": "172.19.3.254",
924+ }],
925+ "ip_address": "172.19.1.34", "id": "network0"
926+ }],
927+ "links": [
928+ {
929+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
930+ "mtu": None, "type": "bridge", "id":
931+ "tap1a81968a-79",
932+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
933+ },
934+ ],
935+ },
936+ 'in_macs': {
937+ 'fa:16:3e:ed:9a:59': 'eth0',
938+ },
939+ 'out_sysconfig': [
940+ ('etc/sysconfig/network-scripts/ifcfg-eth0',
941+ """
942+# Created by cloud-init on instance boot automatically, do not edit.
943+#
944+BOOTPROTO=static
945+DEFROUTE=yes
946+DEVICE=eth0
947+GATEWAY=172.19.3.254
948+HWADDR=fa:16:3e:ed:9a:59
949+IPADDR=172.19.1.34
950+NETMASK=255.255.252.0
951+NM_CONTROLLED=no
952+ONBOOT=yes
953+TYPE=Ethernet
954+USERCTL=no
955+""".lstrip()),
956+ ('etc/sysconfig/network-scripts/route-eth0',
957+ """
958+# Created by cloud-init on instance boot automatically, do not edit.
959+#
960+ADDRESS0=0.0.0.0
961+GATEWAY0=172.19.3.254
962+NETMASK0=0.0.0.0
963+""".lstrip()),
964+ ('etc/resolv.conf',
965+ """
966+; Created by cloud-init on instance boot automatically, do not edit.
967+;
968+nameserver 172.19.0.12
969+""".lstrip()),
970+ ('etc/udev/rules.d/70-persistent-net.rules',
971+ "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
972+ 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
973+ }
974+]
975+
976+
977+def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info,
978+ mock_sys_dev_path):
979+ mock_get_devicelist.return_value = ['eth1000']
980+ dev_characteristics = {
981+ 'eth1000': {
982+ "bridge": False,
983+ "carrier": False,
984+ "dormant": False,
985+ "operstate": "down",
986+ "address": "07-1C-C6-75-A4-BE",
987+ }
988+ }
989+
990+ def netdev_info(name, field):
991+ return dev_characteristics[name][field]
992+
993+ mock_sys_netdev_info.side_effect = netdev_info
994+
995+ def sys_dev_path(devname, path=""):
996+ return tmp_dir + devname + "/" + path
997+
998+ for dev in dev_characteristics:
999+ os.makedirs(os.path.join(tmp_dir, dev))
1000+ with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
1001+ fh.write("down")
1002+
1003+ mock_sys_dev_path.side_effect = sys_dev_path
1004+
1005+
1006+class TestSysConfigRendering(TestCase):
1007+
1008+ @mock.patch("cloudinit.net.sys_dev_path")
1009+ @mock.patch("cloudinit.net.sys_netdev_info")
1010+ @mock.patch("cloudinit.net.get_devicelist")
1011+ def test_default_generation(self, mock_get_devicelist,
1012+ mock_sys_netdev_info,
1013+ mock_sys_dev_path):
1014+ tmp_dir = tempfile.mkdtemp()
1015+ self.addCleanup(shutil.rmtree, tmp_dir)
1016+ _setup_test(tmp_dir, mock_get_devicelist,
1017+ mock_sys_netdev_info, mock_sys_dev_path)
1018+
1019+ network_cfg = net.generate_fallback_config()
1020+ ns = network_state.parse_net_config_data(network_cfg,
1021+ skip_broken=False)
1022+
1023+ render_dir = os.path.join(tmp_dir, "render")
1024+ os.makedirs(render_dir)
1025+
1026+ renderer = sysconfig.Renderer()
1027+ renderer.render_network_state(render_dir, ns)
1028+
1029+ render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000'
1030+ with open(os.path.join(render_dir, render_file)) as fh:
1031+ content = fh.read()
1032+ expected_content = """
1033+# Created by cloud-init on instance boot automatically, do not edit.
1034+#
1035+BOOTPROTO=dhcp
1036+DEVICE=eth1000
1037+HWADDR=07-1C-C6-75-A4-BE
1038+NM_CONTROLLED=no
1039+ONBOOT=yes
1040+TYPE=Ethernet
1041+USERCTL=no
1042+""".lstrip()
1043+ self.assertEqual(expected_content, content)
1044+
1045+ def test_openstack_rendering_samples(self):
1046+ tmp_dir = tempfile.mkdtemp()
1047+ self.addCleanup(shutil.rmtree, tmp_dir)
1048+ render_dir = os.path.join(tmp_dir, "render")
1049+ for os_sample in OS_SAMPLES:
1050+ ex_input = os_sample['in_data']
1051+ ex_mac_addrs = os_sample['in_macs']
1052+ network_cfg = openstack.convert_net_json(
1053+ ex_input, known_macs=ex_mac_addrs)
1054+ ns = network_state.parse_net_config_data(network_cfg,
1055+ skip_broken=False)
1056+ renderer = sysconfig.Renderer()
1057+ renderer.render_network_state(render_dir, ns)
1058+ for fn, expected_content in os_sample.get('out_sysconfig', []):
1059+ with open(os.path.join(render_dir, fn)) as fh:
1060+ self.assertEqual(expected_content, fh.read())
1061+
1062
1063 class TestEniNetRendering(TestCase):
1064
1065@@ -83,35 +236,10 @@
1066 def test_default_generation(self, mock_get_devicelist,
1067 mock_sys_netdev_info,
1068 mock_sys_dev_path):
1069- mock_get_devicelist.return_value = ['eth1000', 'lo']
1070-
1071- dev_characteristics = {
1072- 'eth1000': {
1073- "bridge": False,
1074- "carrier": False,
1075- "dormant": False,
1076- "operstate": "down",
1077- "address": "07-1C-C6-75-A4-BE",
1078- }
1079- }
1080-
1081- def netdev_info(name, field):
1082- return dev_characteristics[name][field]
1083-
1084- mock_sys_netdev_info.side_effect = netdev_info
1085-
1086 tmp_dir = tempfile.mkdtemp()
1087 self.addCleanup(shutil.rmtree, tmp_dir)
1088-
1089- def sys_dev_path(devname, path=""):
1090- return tmp_dir + devname + "/" + path
1091-
1092- for dev in dev_characteristics:
1093- os.makedirs(os.path.join(tmp_dir, dev))
1094- with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
1095- fh.write("down")
1096-
1097- mock_sys_dev_path.side_effect = sys_dev_path
1098+ _setup_test(tmp_dir, mock_get_devicelist,
1099+ mock_sys_netdev_info, mock_sys_dev_path)
1100
1101 network_cfg = net.generate_fallback_config()
1102 ns = network_state.parse_net_config_data(network_cfg,
1103@@ -120,11 +248,11 @@
1104 render_dir = os.path.join(tmp_dir, "render")
1105 os.makedirs(render_dir)
1106
1107- renderer = eni.Renderer()
1108- renderer.render_network_state(render_dir, ns,
1109- eni="interfaces",
1110- links_prefix=None,
1111- netrules=None)
1112+ renderer = eni.Renderer(
1113+ {'links_path_prefix': None,
1114+ 'eni_path': 'interfaces', 'netrules_path': None,
1115+ })
1116+ renderer.render_network_state(render_dir, ns)
1117
1118 self.assertTrue(os.path.exists(os.path.join(render_dir,
1119 'interfaces')))