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

Proposed by Joshua Harlow
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 Pending
Review via email: mp+297115@code.launchpad.net
To post a comment you must log in.
1235. By Joshua Harlow

Add a sysconfig rendering test

1236. By Joshua Harlow

Add a bunch more sample tests for sysconfig

1237. By Joshua Harlow

Refactor some of sysconfig changes -> network_state module

1238. By Joshua Harlow

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

Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Joshua Harlow (harlowja) :
Revision history for this message
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.

Revision history for this message
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

Fixup code review comments

1240. By Joshua Harlow

Fix line length issues

Revision history for this message
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')))