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
=== modified file 'cloudinit/distros/debian.py'
--- cloudinit/distros/debian.py 2016-06-10 21:40:05 +0000
+++ cloudinit/distros/debian.py 2016-06-15 23:16:02 +0000
@@ -57,7 +57,11 @@
57 # should only happen say once per instance...)57 # should only happen say once per instance...)
58 self._runner = helpers.Runners(paths)58 self._runner = helpers.Runners(paths)
59 self.osfamily = 'debian'59 self.osfamily = 'debian'
60 self._net_renderer = eni.Renderer()60 self._net_renderer = eni.Renderer({
61 'eni_path': self.network_conf_fn,
62 'links_prefix_path': self.links_prefix,
63 'netrules_path': None,
64 })
6165
62 def apply_locale(self, locale, out_fn=None):66 def apply_locale(self, locale, out_fn=None):
63 if not out_fn:67 if not out_fn:
@@ -81,13 +85,17 @@
81 return ['all']85 return ['all']
8286
83 def _write_network_config(self, netconfig):87 def _write_network_config(self, netconfig):
88<<<<<<< TREE
84 ns = parse_net_config_data(netconfig)89 ns = parse_net_config_data(netconfig)
85 self._net_renderer.render_network_state(90 self._net_renderer.render_network_state(
86 target="/", network_state=ns,91 target="/", network_state=ns,
87 eni=self.network_conf_fn, links_prefix=self.links_prefix,92 eni=self.network_conf_fn, links_prefix=self.links_prefix,
88 netrules=None)93 netrules=None)
94=======
95 ns = net.parse_net_config_data(netconfig)
96 self._net_renderer.render_network_state("/", ns)
97>>>>>>> MERGE-SOURCE
89 _maybe_remove_legacy_eth0()98 _maybe_remove_legacy_eth0()
90
91 return []99 return []
92100
93 def _bring_up_interfaces(self, device_names):101 def _bring_up_interfaces(self, device_names):
94102
=== modified file 'cloudinit/distros/rhel.py'
--- cloudinit/distros/rhel.py 2015-06-02 20:27:57 +0000
+++ cloudinit/distros/rhel.py 2016-06-15 23:16:02 +0000
@@ -23,6 +23,8 @@
23from cloudinit import distros23from cloudinit import distros
24from cloudinit import helpers24from cloudinit import helpers
25from cloudinit import log as logging25from cloudinit import log as logging
26from cloudinit.net.network_state import parse_net_config_data
27from cloudinit.net import sysconfig
26from cloudinit import util28from cloudinit import util
2729
28from cloudinit.distros import net_util30from cloudinit.distros import net_util
@@ -59,10 +61,16 @@
59 # should only happen say once per instance...)61 # should only happen say once per instance...)
60 self._runner = helpers.Runners(paths)62 self._runner = helpers.Runners(paths)
61 self.osfamily = 'redhat'63 self.osfamily = 'redhat'
64 self._net_renderer = sysconfig.Renderer()
6265
63 def install_packages(self, pkglist):66 def install_packages(self, pkglist):
64 self.package_command('install', pkgs=pkglist)67 self.package_command('install', pkgs=pkglist)
6568
69 def _write_network_config(self, netconfig):
70 ns = parse_net_config_data(netconfig)
71 self._net_renderer.render_network_state("/", ns)
72 return []
73
66 def _write_network(self, settings):74 def _write_network(self, settings):
67 # TODO(harlowja) fix this... since this is the ubuntu format75 # TODO(harlowja) fix this... since this is the ubuntu format
68 entries = net_util.translate_network(settings)76 entries = net_util.translate_network(settings)
6977
=== modified file 'cloudinit/net/__init__.py'
--- cloudinit/net/__init__.py 2016-06-07 01:42:29 +0000
+++ cloudinit/net/__init__.py 2016-06-15 23:16:02 +0000
@@ -26,7 +26,6 @@
26LOG = logging.getLogger(__name__)26LOG = logging.getLogger(__name__)
27SYS_CLASS_NET = "/sys/class/net/"27SYS_CLASS_NET = "/sys/class/net/"
28DEFAULT_PRIMARY_INTERFACE = 'eth0'28DEFAULT_PRIMARY_INTERFACE = 'eth0'
29LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
3029
3130
32def sys_dev_path(devname, path=""):31def sys_dev_path(devname, path=""):
3332
=== modified file 'cloudinit/net/eni.py'
--- cloudinit/net/eni.py 2016-06-14 19:18:53 +0000
+++ cloudinit/net/eni.py 2016-06-15 23:16:02 +0000
@@ -16,10 +16,9 @@
16import os16import os
17import re17import re
1818
19from . import LINKS_FNAME_PREFIX
20from . import ParserError19from . import ParserError
2120
22from .udev import generate_udev_rule21from . import renderer
2322
24from cloudinit import util23from cloudinit import util
2524
@@ -297,21 +296,17 @@
297 'config': [devs[d] for d in sorted(devs)]}296 'config': [devs[d] for d in sorted(devs)]}
298297
299298
300class Renderer(object):299class Renderer(renderer.Renderer):
301 """Renders network information in a /etc/network/interfaces format."""300 """Renders network information in a /etc/network/interfaces format."""
302301
303 def _render_persistent_net(self, network_state):302 def __init__(self, config=None):
304 """Given state, emit udev rules to map mac to ifname."""303 if not config:
305 content = ""304 config = {}
306 interfaces = network_state.get('interfaces')305 self.eni_path = config.get('eni_path', 'etc/network/interfaces')
307 for iface in interfaces.values():306 self.links_path_prefix = config.get(
308 # for physical interfaces write out a persist net udev rule307 'links_path_prefix', 'etc/systemd/network/50-cloud-init-')
309 if iface['type'] == 'physical' and \308 self.netrules_path = config.get(
310 'name' in iface and iface.get('mac_address'):309 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
311 content += generate_udev_rule(iface['name'],
312 iface['mac_address'])
313
314 return content
315310
316 def _render_route(self, route, indent=""):311 def _render_route(self, route, indent=""):
317 """When rendering routes for an iface, in some cases applying a route312 """When rendering routes for an iface, in some cases applying a route
@@ -360,7 +355,15 @@
360 '''Given state, emit etc/network/interfaces content.'''355 '''Given state, emit etc/network/interfaces content.'''
361356
362 content = ""357 content = ""
363 interfaces = network_state.get('interfaces')358 content += "auto lo\niface lo inet loopback\n"
359
360 nameservers = network_state.dns_nameservers
361 if nameservers:
362 content += " dns-nameservers %s\n" % (" ".join(nameservers))
363 searchdomains = network_state.dns_searchdomains
364 if searchdomains:
365 content += " dns-search %s\n" % (" ".join(searchdomains))
366
364 ''' Apply a sort order to ensure that we write out367 ''' Apply a sort order to ensure that we write out
365 the physical interfaces first; this is critical for368 the physical interfaces first; this is critical for
366 bonding369 bonding
@@ -371,12 +374,7 @@
371 'bridge': 2,374 'bridge': 2,
372 'vlan': 3,375 'vlan': 3,
373 }376 }
374 content += "auto lo\niface lo inet loopback\n"377 for iface in sorted(network_state.iter_interfaces(),
375 for dnskey, value in network_state.get('dns', {}).items():
376 if len(value):
377 content += " dns-{} {}\n".format(dnskey, " ".join(value))
378
379 for iface in sorted(interfaces.values(),
380 key=lambda k: (order[k['type']], k['name'])):378 key=lambda k: (order[k['type']], k['name'])):
381379
382 if content[-2:] != "\n\n":380 if content[-2:] != "\n\n":
@@ -409,40 +407,33 @@
409 content += "iface {name} {inet} {mode}\n".format(**iface)407 content += "iface {name} {inet} {mode}\n".format(**iface)
410 content += _iface_add_attrs(iface)408 content += _iface_add_attrs(iface)
411409
412 for route in network_state.get('routes'):410 for route in network_state.iter_routes():
413 content += self._render_route(route)411 content += self._render_route(route)
414412
415 # global replacements until v2 format413 # global replacements until v2 format
416 content = content.replace('mac_address', 'hwaddress')414 content = content.replace('mac_address', 'hwaddress')
417 return content415 return content
418416
419 def render_network_state(417 def render_network_state(self, target, network_state):
420 self, target, network_state, eni="etc/network/interfaces",418 fpeni = os.path.join(target, self.eni_path)
421 links_prefix=LINKS_FNAME_PREFIX,
422 netrules='etc/udev/rules.d/70-persistent-net.rules',
423 writer=None):
424
425 fpeni = os.path.sep.join((target, eni,))
426 util.ensure_dir(os.path.dirname(fpeni))419 util.ensure_dir(os.path.dirname(fpeni))
427 util.write_file(fpeni, self._render_interfaces(network_state))420 util.write_file(fpeni, self._render_interfaces(network_state))
428421
429 if netrules:422 if self.netrules_path:
430 netrules = os.path.sep.join((target, netrules,))423 netrules = os.path.join(target, self.netrules_path)
431 util.ensure_dir(os.path.dirname(netrules))424 util.ensure_dir(os.path.dirname(netrules))
432 util.write_file(netrules,425 util.write_file(netrules,
433 self._render_persistent_net(network_state))426 self._render_persistent_net(network_state))
434427
435 if links_prefix:428 if self.links_path_prefix:
436 self._render_systemd_links(target, network_state,429 self._render_systemd_links(target, network_state,
437 links_prefix=links_prefix)430 links_prefix=self.links_path_prefix)
438431
439 def _render_systemd_links(self, target, network_state,432 def _render_systemd_links(self, target, network_state, links_prefix):
440 links_prefix=LINKS_FNAME_PREFIX):433 fp_prefix = os.path.join(target, links_prefix)
441 fp_prefix = os.path.sep.join((target, links_prefix))
442 for f in glob.glob(fp_prefix + "*"):434 for f in glob.glob(fp_prefix + "*"):
443 os.unlink(f)435 os.unlink(f)
444 interfaces = network_state.get('interfaces')436 for iface in network_state.iter_interfaces():
445 for iface in interfaces.values():
446 if (iface['type'] == 'physical' and 'name' in iface and437 if (iface['type'] == 'physical' and 'name' in iface and
447 iface.get('mac_address')):438 iface.get('mac_address')):
448 fname = fp_prefix + iface['name'] + ".link"439 fname = fp_prefix + iface['name'] + ".link"
449440
=== modified file 'cloudinit/net/network_state.py'
--- cloudinit/net/network_state.py 2016-06-07 17:59:27 +0000
+++ cloudinit/net/network_state.py 2016-06-15 23:16:02 +0000
@@ -38,10 +38,10 @@
38 """38 """
39 state = None39 state = None
40 if 'version' in net_config and 'config' in net_config:40 if 'version' in net_config and 'config' in net_config:
41 ns = NetworkState(version=net_config.get('version'),41 nsi = NetworkStateInterpreter(version=net_config.get('version'),
42 config=net_config.get('config'))42 config=net_config.get('config'))
43 ns.parse_config(skip_broken=skip_broken)43 nsi.parse_config(skip_broken=skip_broken)
44 state = ns.network_state44 state = nsi.network_state
45 return state45 return state
4646
4747
@@ -57,11 +57,10 @@
5757
5858
59def from_state_file(state_file):59def from_state_file(state_file):
60 network_state = None
61 state = util.read_conf(state_file)60 state = util.read_conf(state_file)
62 network_state = NetworkState()61 nsi = NetworkStateInterpreter()
63 network_state.load(state)62 nsi.load(state)
64 return network_state63 return nsi
6564
6665
67def diff_keys(expected, actual):66def diff_keys(expected, actual):
@@ -113,8 +112,50 @@
113 parents, dct)112 parents, dct)
114113
115114
115class NetworkState(object):
116
117 def __init__(self, network_state, version=NETWORK_STATE_VERSION):
118 self._network_state = copy.deepcopy(network_state)
119 self._version = version
120
121 @property
122 def version(self):
123 return self._version
124
125 def iter_routes(self, filter_func=None):
126 for route in self._network_state.get('routes', []):
127 if filter_func is not None:
128 if filter_func(route):
129 yield route
130 else:
131 yield route
132
133 @property
134 def dns_nameservers(self):
135 try:
136 return self._network_state['dns']['nameservers']
137 except KeyError:
138 return []
139
140 @property
141 def dns_searchdomains(self):
142 try:
143 return self._network_state['dns']['search']
144 except KeyError:
145 return []
146
147 def iter_interfaces(self, filter_func=None):
148 ifaces = self._network_state.get('interfaces', {})
149 for iface in six.itervalues(ifaces):
150 if filter_func is None:
151 yield iface
152 else:
153 if filter_func(iface):
154 yield iface
155
156
116@six.add_metaclass(CommandHandlerMeta)157@six.add_metaclass(CommandHandlerMeta)
117class NetworkState(object):158class NetworkStateInterpreter(object):
118159
119 initial_network_state = {160 initial_network_state = {
120 'interfaces': {},161 'interfaces': {},
@@ -126,22 +167,27 @@
126 }167 }
127168
128 def __init__(self, version=NETWORK_STATE_VERSION, config=None):169 def __init__(self, version=NETWORK_STATE_VERSION, config=None):
129 self.version = version170 self._version = version
130 self.config = config171 self._config = config
131 self.network_state = copy.deepcopy(self.initial_network_state)172 self._network_state = copy.deepcopy(self.initial_network_state)
173 self._parsed = False
174
175 @property
176 def network_state(self):
177 return NetworkState(self._network_state, version=self._version)
132178
133 def dump(self):179 def dump(self):
134 state = {180 state = {
135 'version': self.version,181 'version': self._version,
136 'config': self.config,182 'config': self._config,
137 'network_state': self.network_state,183 'network_state': self._network_state,
138 }184 }
139 return util.yaml_dumps(state)185 return util.yaml_dumps(state)
140186
141 def load(self, state):187 def load(self, state):
142 if 'version' not in state:188 if 'version' not in state:
143 LOG.error('Invalid state, missing version field')189 LOG.error('Invalid state, missing version field')
144 raise Exception('Invalid state, missing version field')190 raise ValueError('Invalid state, missing version field')
145191
146 required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]192 required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
147 missing_keys = diff_keys(required_keys, state)193 missing_keys = diff_keys(required_keys, state)
@@ -155,11 +201,11 @@
155 setattr(self, key, state[key])201 setattr(self, key, state[key])
156202
157 def dump_network_state(self):203 def dump_network_state(self):
158 return util.yaml_dumps(self.network_state)204 return util.yaml_dumps(self._network_state)
159205
160 def parse_config(self, skip_broken=True):206 def parse_config(self, skip_broken=True):
161 # rebuild network state207 # rebuild network state
162 for command in self.config:208 for command in self._config:
163 command_type = command['type']209 command_type = command['type']
164 try:210 try:
165 handler = self.command_handlers[command_type]211 handler = self.command_handlers[command_type]
@@ -189,7 +235,7 @@
189 }235 }
190 '''236 '''
191237
192 interfaces = self.network_state.get('interfaces')238 interfaces = self._network_state.get('interfaces', {})
193 iface = interfaces.get(command['name'], {})239 iface = interfaces.get(command['name'], {})
194 for param, val in command.get('params', {}).items():240 for param, val in command.get('params', {}).items():
195 iface.update({param: val})241 iface.update({param: val})
@@ -215,7 +261,7 @@
215 'gateway': None,261 'gateway': None,
216 'subnets': subnets,262 'subnets': subnets,
217 })263 })
218 self.network_state['interfaces'].update({command.get('name'): iface})264 self._network_state['interfaces'].update({command.get('name'): iface})
219 self.dump_network_state()265 self.dump_network_state()
220266
221 @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])267 @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
@@ -228,7 +274,7 @@
228 hwaddress ether BC:76:4E:06:96:B3274 hwaddress ether BC:76:4E:06:96:B3
229 vlan-raw-device eth0275 vlan-raw-device eth0
230 '''276 '''
231 interfaces = self.network_state.get('interfaces')277 interfaces = self._network_state.get('interfaces', {})
232 self.handle_physical(command)278 self.handle_physical(command)
233 iface = interfaces.get(command.get('name'), {})279 iface = interfaces.get(command.get('name'), {})
234 iface['vlan-raw-device'] = command.get('vlan_link')280 iface['vlan-raw-device'] = command.get('vlan_link')
@@ -263,12 +309,12 @@
263 '''309 '''
264310
265 self.handle_physical(command)311 self.handle_physical(command)
266 interfaces = self.network_state.get('interfaces')312 interfaces = self._network_state.get('interfaces')
267 iface = interfaces.get(command.get('name'), {})313 iface = interfaces.get(command.get('name'), {})
268 for param, val in command.get('params').items():314 for param, val in command.get('params').items():
269 iface.update({param: val})315 iface.update({param: val})
270 iface.update({'bond-slaves': 'none'})316 iface.update({'bond-slaves': 'none'})
271 self.network_state['interfaces'].update({iface['name']: iface})317 self._network_state['interfaces'].update({iface['name']: iface})
272318
273 # handle bond slaves319 # handle bond slaves
274 for ifname in command.get('bond_interfaces'):320 for ifname in command.get('bond_interfaces'):
@@ -280,13 +326,13 @@
280 # inject placeholder326 # inject placeholder
281 self.handle_physical(cmd)327 self.handle_physical(cmd)
282328
283 interfaces = self.network_state.get('interfaces')329 interfaces = self._network_state.get('interfaces', {})
284 bond_if = interfaces.get(ifname)330 bond_if = interfaces.get(ifname)
285 bond_if['bond-master'] = command.get('name')331 bond_if['bond-master'] = command.get('name')
286 # copy in bond config into slave332 # copy in bond config into slave
287 for param, val in command.get('params').items():333 for param, val in command.get('params').items():
288 bond_if.update({param: val})334 bond_if.update({param: val})
289 self.network_state['interfaces'].update({ifname: bond_if})335 self._network_state['interfaces'].update({ifname: bond_if})
290336
291 @ensure_command_keys(['name', 'bridge_interfaces', 'params'])337 @ensure_command_keys(['name', 'bridge_interfaces', 'params'])
292 def handle_bridge(self, command):338 def handle_bridge(self, command):
@@ -319,7 +365,7 @@
319365
320 # find one of the bridge port ifaces to get mac_addr366 # find one of the bridge port ifaces to get mac_addr
321 # handle bridge_slaves367 # handle bridge_slaves
322 interfaces = self.network_state.get('interfaces')368 interfaces = self._network_state.get('interfaces', {})
323 for ifname in command.get('bridge_interfaces'):369 for ifname in command.get('bridge_interfaces'):
324 if ifname in interfaces:370 if ifname in interfaces:
325 continue371 continue
@@ -330,7 +376,7 @@
330 # inject placeholder376 # inject placeholder
331 self.handle_physical(cmd)377 self.handle_physical(cmd)
332378
333 interfaces = self.network_state.get('interfaces')379 interfaces = self._network_state.get('interfaces', {})
334 self.handle_physical(command)380 self.handle_physical(command)
335 iface = interfaces.get(command.get('name'), {})381 iface = interfaces.get(command.get('name'), {})
336 iface['bridge_ports'] = command['bridge_interfaces']382 iface['bridge_ports'] = command['bridge_interfaces']
@@ -341,7 +387,7 @@
341387
342 @ensure_command_keys(['address'])388 @ensure_command_keys(['address'])
343 def handle_nameserver(self, command):389 def handle_nameserver(self, command):
344 dns = self.network_state.get('dns')390 dns = self._network_state.get('dns')
345 if 'address' in command:391 if 'address' in command:
346 addrs = command['address']392 addrs = command['address']
347 if not type(addrs) == list:393 if not type(addrs) == list:
@@ -357,7 +403,7 @@
357403
358 @ensure_command_keys(['destination'])404 @ensure_command_keys(['destination'])
359 def handle_route(self, command):405 def handle_route(self, command):
360 routes = self.network_state.get('routes')406 routes = self._network_state.get('routes', [])
361 network, cidr = command['destination'].split("/")407 network, cidr = command['destination'].split("/")
362 netmask = cidr2mask(int(cidr))408 netmask = cidr2mask(int(cidr))
363 route = {409 route = {
364410
=== added file 'cloudinit/net/renderer.py'
--- cloudinit/net/renderer.py 1970-01-01 00:00:00 +0000
+++ cloudinit/net/renderer.py 2016-06-15 23:16:02 +0000
@@ -0,0 +1,48 @@
1# Copyright (C) 2013-2014 Canonical Ltd.
2#
3# Author: Scott Moser <scott.moser@canonical.com>
4# Author: Blake Rouse <blake.rouse@canonical.com>
5#
6# Curtin is free software: you can redistribute it and/or modify it under
7# the terms of the GNU Affero General Public License as published by the
8# Free Software Foundation, either version 3 of the License, or (at your
9# option) any later version.
10#
11# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
14# more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
18
19import six
20
21from .udev import generate_udev_rule
22
23
24def filter_by_type(match_type):
25 return lambda iface: match_type == iface['type']
26
27
28def filter_by_name(match_name):
29 return lambda iface: match_name == iface['name']
30
31
32filter_by_physical = filter_by_type('physical')
33
34
35class Renderer(object):
36
37 @staticmethod
38 def _render_persistent_net(network_state):
39 """Given state, emit udev rules to map mac to ifname."""
40 # TODO(harlowja): this seems shared between eni renderer and
41 # this, so move it to a shared location.
42 content = six.StringIO()
43 for iface in network_state.iter_interfaces(filter_by_physical):
44 # for physical interfaces write out a persist net udev rule
45 if 'name' in iface and iface.get('mac_address'):
46 content.write(generate_udev_rule(iface['name'],
47 iface['mac_address']))
48 return content.getvalue()
049
=== added file 'cloudinit/net/sysconfig.py'
--- cloudinit/net/sysconfig.py 1970-01-01 00:00:00 +0000
+++ cloudinit/net/sysconfig.py 2016-06-15 23:16:02 +0000
@@ -0,0 +1,400 @@
1# vi: ts=4 expandtab
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License version 3, as
5# published by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15import os
16import re
17
18import six
19
20from cloudinit.distros.parsers import resolv_conf
21from cloudinit import util
22
23from . import renderer
24
25
26def _make_header(sep='#'):
27 lines = [
28 "Created by cloud-init on instance boot automatically, do not edit.",
29 "",
30 ]
31 for i in range(0, len(lines)):
32 if lines[i]:
33 lines[i] = sep + " " + lines[i]
34 else:
35 lines[i] = sep
36 return "\n".join(lines)
37
38
39def _is_default_route(route):
40 if route['network'] == '::' and route['netmask'] == 0:
41 return True
42 if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
43 return True
44 return False
45
46
47def _quote_value(value):
48 if re.search(r"\s", value):
49 # This doesn't handle complex cases...
50 if value.startswith('"') and value.endswith('"'):
51 return value
52 else:
53 return '"%s"' % value
54 else:
55 return value
56
57
58class ConfigMap(object):
59 """Sysconfig like dictionary object."""
60
61 # Why does redhat prefer yes/no to true/false??
62 _bool_map = {
63 True: 'yes',
64 False: 'no',
65 }
66
67 def __init__(self):
68 self._conf = {}
69
70 def __setitem__(self, key, value):
71 self._conf[key] = value
72
73 def drop(self, key):
74 self._conf.pop(key, None)
75
76 def __len__(self):
77 return len(self._conf)
78
79 def to_string(self):
80 buf = six.StringIO()
81 buf.write(_make_header())
82 if self._conf:
83 buf.write("\n")
84 for key in sorted(self._conf.keys()):
85 value = self._conf[key]
86 if isinstance(value, bool):
87 value = self._bool_map[value]
88 if not isinstance(value, six.string_types):
89 value = str(value)
90 buf.write("%s=%s\n" % (key, _quote_value(value)))
91 return buf.getvalue()
92
93
94class Route(ConfigMap):
95 """Represents a route configuration."""
96
97 route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
98
99 def __init__(self, route_name, base_sysconf_dir):
100 super(Route, self).__init__()
101 self.last_idx = 1
102 self.has_set_default = False
103 self._route_name = route_name
104 self._base_sysconf_dir = base_sysconf_dir
105
106 def copy(self):
107 r = Route(self._route_name, self._base_sysconf_dir)
108 r._conf = self._conf.copy()
109 r.last_idx = self.last_idx
110 r.has_set_default = self.has_set_default
111 return r
112
113 @property
114 def path(self):
115 return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
116 'name': self._route_name})
117
118
119class NetInterface(ConfigMap):
120 """Represents a sysconfig/networking-script (and its config + children)."""
121
122 iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
123
124 iface_types = {
125 'ethernet': 'Ethernet',
126 'bond': 'Bond',
127 'bridge': 'Bridge',
128 }
129
130 def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
131 super(NetInterface, self).__init__()
132 self.children = []
133 self.routes = Route(iface_name, base_sysconf_dir)
134 self._kind = kind
135 self._iface_name = iface_name
136 self._conf['DEVICE'] = iface_name
137 self._conf['TYPE'] = self.iface_types[kind]
138 self._base_sysconf_dir = base_sysconf_dir
139
140 @property
141 def name(self):
142 return self._iface_name
143
144 @name.setter
145 def name(self, iface_name):
146 self._iface_name = iface_name
147 self._conf['DEVICE'] = iface_name
148
149 @property
150 def kind(self):
151 return self._kind
152
153 @kind.setter
154 def kind(self, kind):
155 self._kind = kind
156 self._conf['TYPE'] = self.iface_types[kind]
157
158 @property
159 def path(self):
160 return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
161 'name': self.name})
162
163 def copy(self, copy_children=False, copy_routes=False):
164 c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
165 c._conf = self._conf.copy()
166 if copy_children:
167 c.children = list(self.children)
168 if copy_routes:
169 c.routes = self.routes.copy()
170 return c
171
172
173class Renderer(renderer.Renderer):
174 """Renders network information in a /etc/sysconfig format."""
175
176 # See: https://access.redhat.com/documentation/en-US/\
177 # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
178 # s1-networkscripts-interfaces.html (or other docs for
179 # details about this)
180
181 iface_defaults = tuple([
182 ('ONBOOT', True),
183 ('USERCTL', False),
184 ('NM_CONTROLLED', False),
185 ('BOOTPROTO', 'none'),
186 ])
187
188 # If these keys exist, then there values will be used to form
189 # a BONDING_OPTS grouping; otherwise no grouping will be set.
190 bond_tpl_opts = tuple([
191 ('bond_mode', "mode=%s"),
192 ('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
193 ('bond_miimon', "miimon=%s"),
194 ])
195
196 bridge_opts_keys = tuple([
197 ('bridge_stp', 'STP'),
198 ('bridge_ageing', 'AGEING'),
199 ('bridge_bridgeprio', 'PRIO'),
200 ])
201
202 def __init__(self, config=None):
203 if not config:
204 config = {}
205 self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
206 self.netrules_path = config.get(
207 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
208 self.dns_path = config.get('dns_path', 'etc/resolv.conf')
209
210 @classmethod
211 def _render_iface_shared(cls, iface, iface_cfg):
212 for k, v in cls.iface_defaults:
213 iface_cfg[k] = v
214 for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
215 old_value = iface.get(old_key)
216 if old_value is not None:
217 iface_cfg[new_key] = old_value
218
219 @classmethod
220 def _render_subnet(cls, iface_cfg, route_cfg, subnet):
221 subnet_type = subnet.get('type')
222 if subnet_type == 'dhcp6':
223 iface_cfg['DHCPV6C'] = True
224 iface_cfg['IPV6INIT'] = True
225 iface_cfg['BOOTPROTO'] = 'dhcp'
226 elif subnet_type in ['dhcp4', 'dhcp']:
227 iface_cfg['BOOTPROTO'] = 'dhcp'
228 elif subnet_type == 'static':
229 iface_cfg['BOOTPROTO'] = 'static'
230 if subnet.get('ipv6'):
231 iface_cfg['IPV6ADDR'] = subnet['address']
232 iface_cfg['IPV6INIT'] = True
233 else:
234 iface_cfg['IPADDR'] = subnet['address']
235 else:
236 raise ValueError("Unknown subnet type '%s' found"
237 " for interface '%s'" % (subnet_type,
238 iface_cfg.name))
239 if 'netmask' in subnet:
240 iface_cfg['NETMASK'] = subnet['netmask']
241 for route in subnet.get('routes', []):
242 if _is_default_route(route):
243 if route_cfg.has_set_default:
244 raise ValueError("Duplicate declaration of default"
245 " route found for interface '%s'"
246 % (iface_cfg.name))
247 # NOTE(harlowja): ipv6 and ipv4 default gateways
248 gw_key = 'GATEWAY0'
249 nm_key = 'NETMASK0'
250 addr_key = 'ADDRESS0'
251 # The owning interface provides the default route.
252 #
253 # TODO(harlowja): add validation that no other iface has
254 # also provided the default route?
255 iface_cfg['DEFROUTE'] = True
256 if 'gateway' in route:
257 iface_cfg['GATEWAY'] = route['gateway']
258 route_cfg.has_set_default = True
259 else:
260 gw_key = 'GATEWAY%s' % route_cfg.last_idx
261 nm_key = 'NETMASK%s' % route_cfg.last_idx
262 addr_key = 'ADDRESS%s' % route_cfg.last_idx
263 route_cfg.last_idx += 1
264 for (old_key, new_key) in [('gateway', gw_key),
265 ('netmask', nm_key),
266 ('network', addr_key)]:
267 if old_key in route:
268 route_cfg[new_key] = route[old_key]
269
270 @classmethod
271 def _render_bonding_opts(cls, iface_cfg, iface):
272 bond_opts = []
273 for (bond_key, value_tpl) in cls.bond_tpl_opts:
274 # Seems like either dash or underscore is possible?
275 bond_keys = [bond_key, bond_key.replace("_", "-")]
276 for bond_key in bond_keys:
277 if bond_key in iface:
278 bond_value = iface[bond_key]
279 if isinstance(bond_value, (tuple, list)):
280 bond_value = " ".join(bond_value)
281 bond_opts.append(value_tpl % (bond_value))
282 break
283 if bond_opts:
284 iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
285
286 @classmethod
287 def _render_physical_interfaces(cls, network_state, iface_contents):
288 physical_filter = renderer.filter_by_physical
289 for iface in network_state.iter_interfaces(physical_filter):
290 iface_name = iface['name']
291 iface_subnets = iface.get("subnets", [])
292 iface_cfg = iface_contents[iface_name]
293 route_cfg = iface_cfg.routes
294 if len(iface_subnets) == 1:
295 cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
296 elif len(iface_subnets) > 1:
297 for i, iface_subnet in enumerate(iface_subnets,
298 start=len(iface.children)):
299 iface_sub_cfg = iface_cfg.copy()
300 iface_sub_cfg.name = "%s:%s" % (iface_name, i)
301 iface.children.append(iface_sub_cfg)
302 cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
303
304 @classmethod
305 def _render_bond_interfaces(cls, network_state, iface_contents):
306 bond_filter = renderer.filter_by_type('bond')
307 for iface in network_state.iter_interfaces(bond_filter):
308 iface_name = iface['name']
309 iface_cfg = iface_contents[iface_name]
310 cls._render_bonding_opts(iface_cfg, iface)
311 iface_master_name = iface['bond-master']
312 iface_cfg['MASTER'] = iface_master_name
313 iface_cfg['SLAVE'] = True
314 # Ensure that the master interface (and any of its children)
315 # are actually marked as being bond types...
316 master_cfg = iface_contents[iface_master_name]
317 master_cfgs = [master_cfg]
318 master_cfgs.extend(master_cfg.children)
319 for master_cfg in master_cfgs:
320 master_cfg['BONDING_MASTER'] = True
321 master_cfg.kind = 'bond'
322
323 @staticmethod
324 def _render_vlan_interfaces(network_state, iface_contents):
325 vlan_filter = renderer.filter_by_type('vlan')
326 for iface in network_state.iter_interfaces(vlan_filter):
327 iface_name = iface['name']
328 iface_cfg = iface_contents[iface_name]
329 iface_cfg['VLAN'] = True
330 iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
331
332 @staticmethod
333 def _render_dns(network_state, existing_dns_path=None):
334 content = resolv_conf.ResolvConf("")
335 if existing_dns_path and os.path.isfile(existing_dns_path):
336 content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
337 for nameserver in network_state.dns_nameservers:
338 content.add_nameserver(nameserver)
339 for searchdomain in network_state.dns_searchdomains:
340 content.add_search_domain(searchdomain)
341 return "\n".join([_make_header(';'), str(content)])
342
343 @classmethod
344 def _render_bridge_interfaces(cls, network_state, iface_contents):
345 bridge_filter = renderer.filter_by_type('bridge')
346 for iface in network_state.iter_interfaces(bridge_filter):
347 iface_name = iface['name']
348 iface_cfg = iface_contents[iface_name]
349 iface_cfg.kind = 'bridge'
350 for old_key, new_key in cls.bridge_opts_keys:
351 if old_key in iface:
352 iface_cfg[new_key] = iface[old_key]
353 # Is this the right key to get all the connected interfaces?
354 for bridged_iface_name in iface.get('bridge_ports', []):
355 # Ensure all bridged interfaces are correctly tagged
356 # as being bridged to this interface.
357 bridged_cfg = iface_contents[bridged_iface_name]
358 bridged_cfgs = [bridged_cfg]
359 bridged_cfgs.extend(bridged_cfg.children)
360 for bridge_cfg in bridged_cfgs:
361 bridge_cfg['BRIDGE'] = iface_name
362
363 @classmethod
364 def _render_sysconfig(cls, base_sysconf_dir, network_state):
365 '''Given state, return /etc/sysconfig files + contents'''
366 iface_contents = {}
367 for iface in network_state.iter_interfaces():
368 iface_name = iface['name']
369 iface_cfg = NetInterface(iface_name, base_sysconf_dir)
370 cls._render_iface_shared(iface, iface_cfg)
371 iface_contents[iface_name] = iface_cfg
372 cls._render_physical_interfaces(network_state, iface_contents)
373 cls._render_bond_interfaces(network_state, iface_contents)
374 cls._render_vlan_interfaces(network_state, iface_contents)
375 cls._render_bridge_interfaces(network_state, iface_contents)
376 contents = {}
377 for iface_name, iface_cfg in iface_contents.items():
378 if iface_cfg or iface_cfg.children:
379 contents[iface_cfg.path] = iface_cfg.to_string()
380 for iface_cfg in iface_cfg.children:
381 if iface_cfg:
382 contents[iface_cfg.path] = iface_cfg.to_string()
383 if iface_cfg.routes:
384 contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
385 return contents
386
387 def render_network_state(self, target, network_state):
388 base_sysconf_dir = os.path.join(target, self.sysconf_dir)
389 for path, data in self._render_sysconfig(base_sysconf_dir,
390 network_state).items():
391 util.write_file(path, data)
392 if self.dns_path:
393 dns_path = os.path.join(target, self.dns_path)
394 resolv_content = self._render_dns(network_state,
395 existing_dns_path=dns_path)
396 util.write_file(dns_path, resolv_content)
397 if self.netrules_path:
398 netrules_content = self._render_persistent_net(network_state)
399 netrules_path = os.path.join(target, self.netrules_path)
400 util.write_file(netrules_path, netrules_content)
0401
=== modified file 'tests/unittests/test_net.py'
--- tests/unittests/test_net.py 2016-05-19 22:33:15 +0000
+++ tests/unittests/test_net.py 2016-06-15 23:16:02 +0000
@@ -2,6 +2,8 @@
2from cloudinit.net import cmdline2from cloudinit.net import cmdline
3from cloudinit.net import eni3from cloudinit.net import eni
4from cloudinit.net import network_state4from cloudinit.net import network_state
5from cloudinit.net import sysconfig
6from cloudinit.sources.helpers import openstack
5from cloudinit import util7from cloudinit import util
68
7from .helpers import mock9from .helpers import mock
@@ -74,6 +76,157 @@
74 'dns_nameservers': ['10.0.1.1']}],76 'dns_nameservers': ['10.0.1.1']}],
75}77}
7678
79# Examples (and expected outputs for various renderers).
80OS_SAMPLES = [
81 {
82 'in_data': {
83 "services": [{"type": "dns", "address": "172.19.0.12"}],
84 "networks": [{
85 "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
86 "type": "ipv4", "netmask": "255.255.252.0",
87 "link": "tap1a81968a-79",
88 "routes": [{
89 "netmask": "0.0.0.0",
90 "network": "0.0.0.0",
91 "gateway": "172.19.3.254",
92 }],
93 "ip_address": "172.19.1.34", "id": "network0"
94 }],
95 "links": [
96 {
97 "ethernet_mac_address": "fa:16:3e:ed:9a:59",
98 "mtu": None, "type": "bridge", "id":
99 "tap1a81968a-79",
100 "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
101 },
102 ],
103 },
104 'in_macs': {
105 'fa:16:3e:ed:9a:59': 'eth0',
106 },
107 'out_sysconfig': [
108 ('etc/sysconfig/network-scripts/ifcfg-eth0',
109 """
110# Created by cloud-init on instance boot automatically, do not edit.
111#
112BOOTPROTO=static
113DEFROUTE=yes
114DEVICE=eth0
115GATEWAY=172.19.3.254
116HWADDR=fa:16:3e:ed:9a:59
117IPADDR=172.19.1.34
118NETMASK=255.255.252.0
119NM_CONTROLLED=no
120ONBOOT=yes
121TYPE=Ethernet
122USERCTL=no
123""".lstrip()),
124 ('etc/sysconfig/network-scripts/route-eth0',
125 """
126# Created by cloud-init on instance boot automatically, do not edit.
127#
128ADDRESS0=0.0.0.0
129GATEWAY0=172.19.3.254
130NETMASK0=0.0.0.0
131""".lstrip()),
132 ('etc/resolv.conf',
133 """
134; Created by cloud-init on instance boot automatically, do not edit.
135;
136nameserver 172.19.0.12
137""".lstrip()),
138 ('etc/udev/rules.d/70-persistent-net.rules',
139 "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
140 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
141 }
142]
143
144
145def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info,
146 mock_sys_dev_path):
147 mock_get_devicelist.return_value = ['eth1000']
148 dev_characteristics = {
149 'eth1000': {
150 "bridge": False,
151 "carrier": False,
152 "dormant": False,
153 "operstate": "down",
154 "address": "07-1C-C6-75-A4-BE",
155 }
156 }
157
158 def netdev_info(name, field):
159 return dev_characteristics[name][field]
160
161 mock_sys_netdev_info.side_effect = netdev_info
162
163 def sys_dev_path(devname, path=""):
164 return tmp_dir + devname + "/" + path
165
166 for dev in dev_characteristics:
167 os.makedirs(os.path.join(tmp_dir, dev))
168 with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
169 fh.write("down")
170
171 mock_sys_dev_path.side_effect = sys_dev_path
172
173
174class TestSysConfigRendering(TestCase):
175
176 @mock.patch("cloudinit.net.sys_dev_path")
177 @mock.patch("cloudinit.net.sys_netdev_info")
178 @mock.patch("cloudinit.net.get_devicelist")
179 def test_default_generation(self, mock_get_devicelist,
180 mock_sys_netdev_info,
181 mock_sys_dev_path):
182 tmp_dir = tempfile.mkdtemp()
183 self.addCleanup(shutil.rmtree, tmp_dir)
184 _setup_test(tmp_dir, mock_get_devicelist,
185 mock_sys_netdev_info, mock_sys_dev_path)
186
187 network_cfg = net.generate_fallback_config()
188 ns = network_state.parse_net_config_data(network_cfg,
189 skip_broken=False)
190
191 render_dir = os.path.join(tmp_dir, "render")
192 os.makedirs(render_dir)
193
194 renderer = sysconfig.Renderer()
195 renderer.render_network_state(render_dir, ns)
196
197 render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000'
198 with open(os.path.join(render_dir, render_file)) as fh:
199 content = fh.read()
200 expected_content = """
201# Created by cloud-init on instance boot automatically, do not edit.
202#
203BOOTPROTO=dhcp
204DEVICE=eth1000
205HWADDR=07-1C-C6-75-A4-BE
206NM_CONTROLLED=no
207ONBOOT=yes
208TYPE=Ethernet
209USERCTL=no
210""".lstrip()
211 self.assertEqual(expected_content, content)
212
213 def test_openstack_rendering_samples(self):
214 tmp_dir = tempfile.mkdtemp()
215 self.addCleanup(shutil.rmtree, tmp_dir)
216 render_dir = os.path.join(tmp_dir, "render")
217 for os_sample in OS_SAMPLES:
218 ex_input = os_sample['in_data']
219 ex_mac_addrs = os_sample['in_macs']
220 network_cfg = openstack.convert_net_json(
221 ex_input, known_macs=ex_mac_addrs)
222 ns = network_state.parse_net_config_data(network_cfg,
223 skip_broken=False)
224 renderer = sysconfig.Renderer()
225 renderer.render_network_state(render_dir, ns)
226 for fn, expected_content in os_sample.get('out_sysconfig', []):
227 with open(os.path.join(render_dir, fn)) as fh:
228 self.assertEqual(expected_content, fh.read())
229
77230
78class TestEniNetRendering(TestCase):231class TestEniNetRendering(TestCase):
79232
@@ -83,35 +236,10 @@
83 def test_default_generation(self, mock_get_devicelist,236 def test_default_generation(self, mock_get_devicelist,
84 mock_sys_netdev_info,237 mock_sys_netdev_info,
85 mock_sys_dev_path):238 mock_sys_dev_path):
86 mock_get_devicelist.return_value = ['eth1000', 'lo']
87
88 dev_characteristics = {
89 'eth1000': {
90 "bridge": False,
91 "carrier": False,
92 "dormant": False,
93 "operstate": "down",
94 "address": "07-1C-C6-75-A4-BE",
95 }
96 }
97
98 def netdev_info(name, field):
99 return dev_characteristics[name][field]
100
101 mock_sys_netdev_info.side_effect = netdev_info
102
103 tmp_dir = tempfile.mkdtemp()239 tmp_dir = tempfile.mkdtemp()
104 self.addCleanup(shutil.rmtree, tmp_dir)240 self.addCleanup(shutil.rmtree, tmp_dir)
105241 _setup_test(tmp_dir, mock_get_devicelist,
106 def sys_dev_path(devname, path=""):242 mock_sys_netdev_info, mock_sys_dev_path)
107 return tmp_dir + devname + "/" + path
108
109 for dev in dev_characteristics:
110 os.makedirs(os.path.join(tmp_dir, dev))
111 with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
112 fh.write("down")
113
114 mock_sys_dev_path.side_effect = sys_dev_path
115243
116 network_cfg = net.generate_fallback_config()244 network_cfg = net.generate_fallback_config()
117 ns = network_state.parse_net_config_data(network_cfg,245 ns = network_state.parse_net_config_data(network_cfg,
@@ -120,11 +248,11 @@
120 render_dir = os.path.join(tmp_dir, "render")248 render_dir = os.path.join(tmp_dir, "render")
121 os.makedirs(render_dir)249 os.makedirs(render_dir)
122250
123 renderer = eni.Renderer()251 renderer = eni.Renderer(
124 renderer.render_network_state(render_dir, ns,252 {'links_path_prefix': None,
125 eni="interfaces",253 'eni_path': 'interfaces', 'netrules_path': None,
126 links_prefix=None,254 })
127 netrules=None)255 renderer.render_network_state(render_dir, ns)
128256
129 self.assertTrue(os.path.exists(os.path.join(render_dir,257 self.assertTrue(os.path.exists(os.path.join(render_dir,
130 'interfaces')))258 'interfaces')))