From c706c3a4df9b538864738b804013fb36571efa49 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 3 Jun 2016 14:18:38 -0400 Subject: improve how 'lo' is handled when rendering network state to interfaces if you provide network state with a proper 'lo' entry, then when you render network interfaces you would get 2 entries. the additional one was because we add an 'lo' always and also because we had to put global 'dns' entries there. this fixes that duplicatation by handling lo specifically. --- cloudinit/net/__init__.py | 105 ++++++++++++++----------- tests/unittests/test_net.py | 182 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 44 deletions(-) diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index f47053b2..0066561e 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -17,6 +17,7 @@ # along with Curtin. If not, see . import base64 +import copy import errno import glob import gzip @@ -418,7 +419,7 @@ def render_persistent_net(network_state): # TODO: switch valid_map based on mode inet/inet6 def iface_add_subnet(iface, subnet): - content = "" + content = [] valid_map = [ 'address', 'netmask', @@ -437,14 +438,14 @@ def iface_add_subnet(iface, subnet): value = " ".join(value) if '_' in key: key = key.replace('_', '-') - content += " {} {}\n".format(key, value) + content.append(" {} {}".format(key, value)) return content # TODO: switch to valid_map for attrs def iface_add_attrs(iface): - content = "" + content = [] ignore_map = [ 'control', 'index', @@ -461,7 +462,7 @@ def iface_add_attrs(iface): if value and key not in ignore_map: if type(value) == list: value = " ".join(value) - content += " {} {}\n".format(key, value) + content.append(" {} {}".format(key, value)) return content @@ -481,10 +482,10 @@ def render_route(route, indent=""): 1. http://askubuntu.com/questions/168033/ how-to-set-static-routes-in-ubuntu-server """ - content = "" + content = [] up = indent + "post-up route add" down = indent + "pre-down route del" - eol = " || true\n" + eol = " || true" mapping = { 'network': '-net', 'netmask': 'netmask', @@ -493,20 +494,20 @@ def render_route(route, indent=""): } if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': default_gw = " default gw %s" % route['gateway'] - content += up + default_gw + eol - content += down + default_gw + eol + content.append(up + default_gw + eol) + content.append(down + default_gw + eol) elif route['network'] == '::' and route['netmask'] == 0: # ipv6! default_gw = " -A inet6 default gw %s" % route['gateway'] - content += up + default_gw + eol - content += down + default_gw + eol + content.append(up + default_gw + eol) + content.append(down + default_gw + eol) else: route_line = "" for k in ['network', 'netmask', 'gateway', 'metric']: if k in route: route_line += " %s %s" % (mapping[k], route[k]) - content += up + route_line + eol - content += down + route_line + eol + content.append(up + route_line + eol) + content.append(down + route_line + eol) return content @@ -527,8 +528,38 @@ def iface_start_entry(iface, index): subst = iface.copy() subst.update({'fullname': fullname, 'cverb': cverb}) - return ("{cverb} {fullname}\n" - "iface {fullname} {inet} {mode}\n").format(**subst) + if 'inet' not in subst: + print("bug....iface: %s" % iface) + return ["{cverb} {fullname}".format(**subst), + "iface {fullname} {inet} {mode}".format(**subst)] + + +def _render_iface(iface): + lines = [] + subnets = iface.get('subnets', {}) + if subnets: + for index, subnet in zip(range(0, len(subnets)), subnets): + iface['index'] = index + iface['mode'] = subnet['type'] + iface['control'] = subnet.get('control', 'auto') + if iface['mode'].endswith('6'): + iface['inet'] += '6' + elif iface['mode'] == 'static' and ":" in subnet['address']: + iface['inet'] += '6' + if iface['mode'].startswith('dhcp'): + iface['mode'] = 'dhcp' + + lines.extend(iface_start_entry(iface, index)) + lines.extend(iface_add_subnet(iface, subnet)) + lines.extend(iface_add_attrs(iface)) + lines.append("") + else: + # ifenslave docs say to auto the slave devices + if 'bond-master' in iface: + lines.append("auto {name}".format(**iface)) + lines.append("iface {name} {inet} {mode}".format(**iface)) + lines.extend(iface_add_attrs(iface)) + return lines def render_interfaces(network_state): @@ -546,44 +577,30 @@ def render_interfaces(network_state): 'bridge': 2, 'vlan': 3, } - content += "auto lo\niface lo inet loopback\n" + + # handle 'lo' specifically as we need to insert the global dns entries + # there (as that is the only interface) that will be always up. + lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet', + 'subnets': [{'type': 'loopback', 'control': 'auto'}]} + for iface in interfaces.values(): + if iface.get('name') == "lo": + lo = copy.deepcopy(iface) for dnskey, value in network_state.get('dns', {}).items(): if len(value): - content += " dns-{} {}\n".format(dnskey, " ".join(value)) + lo['subnets'][0]["dns_" + dnskey] = value + + sections = [_render_iface(lo)] for iface in sorted(interfaces.values(), key=lambda k: (order[k['type']], k['name'])): - - if content[-2:] != "\n\n": - content += "\n" - subnets = iface.get('subnets', {}) - if subnets: - for index, subnet in zip(range(0, len(subnets)), subnets): - if content[-2:] != "\n\n": - content += "\n" - iface['index'] = index - iface['mode'] = subnet['type'] - iface['control'] = subnet.get('control', 'auto') - if iface['mode'].endswith('6'): - iface['inet'] += '6' - elif iface['mode'] == 'static' and ":" in subnet['address']: - iface['inet'] += '6' - if iface['mode'].startswith('dhcp'): - iface['mode'] = 'dhcp' - - content += iface_start_entry(iface, index) - content += iface_add_subnet(iface, subnet) - content += iface_add_attrs(iface) - else: - # ifenslave docs say to auto the slave devices - if 'bond-master' in iface: - content += "auto {name}\n".format(**iface) - content += "iface {name} {inet} {mode}\n".format(**iface) - content += iface_add_attrs(iface) + if iface.get('name') == "lo": + continue + sections.append(_render_iface(iface)) for route in network_state.get('routes'): - content += render_route(route) + sections.append(render_route(route)) + content = ''.join(['\n'.join(s) + '\n\n' for s in sections]) # global replacements until v2 format content = content.replace('mac_address', 'hwaddress') return content diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 624a9aa8..34875f7b 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -9,6 +9,8 @@ import gzip import io import json import os +import yaml + DHCP_CONTENT_1 = """ DEVICE='eth0' @@ -68,6 +70,163 @@ STATIC_EXPECTED_1 = { 'dns_nameservers': ['10.0.1.1']}], } +EXAMPLE_ENI = """ +auto lo +iface lo inet loopback + dns-nameservers 10.0.0.1 + dns-search foo.com + +auto eth0 +iface eth0 inet static + address 1.2.3.12 + netmask 255.255.255.248 + broadcast 1.2.3.15 + gateway 1.2.3.9 + dns-nameservers 69.9.160.191 69.9.191.4 +auto eth1 +iface eth1 inet static + address 10.248.2.4 + netmask 255.255.255.248 + broadcast 10.248.2.7 +""" + +NETWORK_YAML_SMALL = """ +version: 1 +config: + # Physical interfaces. + - type: physical + name: eth0 + mac_address: "c0:d6:9f:2c:e8:80" + subnets: + - type: dhcp4 + - type: static + address: 192.168.21.3/24 + dns_nameservers: + - 8.8.8.8 + - 8.8.4.4 + dns_search: barley.maas sach.maas + - type: physical + name: eth1 + mac_address: "cf:d6:af:48:e8:80" + - type: nameserver + address: + - 1.2.3.4 + - 5.6.7.8 + search: + - wark.maas +""" +NETWORK_YAML_ALL = """ +version: 1 +config: + # Physical interfaces. + - type: physical + name: eth0 + mac_address: "c0:d6:9f:2c:e8:80" + - type: physical + name: eth1 + mac_address: "aa:d6:9f:2c:e8:80" + - type: physical + name: eth2 + mac_address: "c0:bb:9f:2c:e8:80" + - type: physical + name: eth3 + mac_address: "66:bb:9f:2c:e8:80" + - type: physical + name: eth4 + mac_address: "98:bb:9f:2c:e8:80" + # specify how ifupdown should treat iface + # control is one of ['auto', 'hotplug', 'manual'] + # with manual meaning ifup/ifdown should not affect the iface + # useful for things like iscsi root + dhcp + - type: physical + name: eth5 + mac_address: "98:bb:9f:2c:e8:8a" + subnets: + - type: dhcp + control: manual + # VLAN interface. + - type: vlan + name: eth0.101 + vlan_link: eth0 + vlan_id: 101 + mtu: 1500 + subnets: + - type: static + address: 192.168.0.2/24 + gateway: 192.168.0.1 + dns_nameservers: + - 192.168.0.10 + - 10.23.23.134 + dns_search: + - barley.maas + - sacchromyces.maas + - brettanomyces.maas + - type: static + address: 192.168.2.10/24 + # Bond. + - type: bond + name: bond0 + # if 'mac_address' is omitted, the MAC is taken from + # the first slave. + mac_address: "aa:bb:cc:dd:ee:ff" + bond_interfaces: + - eth1 + - eth2 + params: + bond-mode: active-backup + subnets: + - type: dhcp6 + # A Bond VLAN. + - type: vlan + name: bond0.200 + vlan_link: bond0 + vlan_id: 200 + subnets: + - type: dhcp4 + # A bridge. + - type: bridge + name: br0 + bridge_interfaces: + - eth3 + - eth4 + ipv4_conf: + rp_filter: 1 + proxy_arp: 0 + forwarding: 1 + ipv6_conf: + autoconf: 1 + disable_ipv6: 1 + use_tempaddr: 1 + forwarding: 1 + # basically anything in /proc/sys/net/ipv6/conf/.../ + params: + bridge_stp: 'off' + bridge_fd: 0 + bridge_maxwait: 0 + subnets: + - type: static + address: 192.168.14.2/24 + - type: static + address: 2001:1::1/64 # default to /64 + # A global nameserver. + - type: nameserver + address: 8.8.8.8 + search: barley.maas + # global nameservers and search in list form + - type: nameserver + address: + - 4.4.4.4 + - 8.8.4.4 + search: + - wark.maas + - foobar.maas + # A global route. + - type: route + destination: 10.0.0.0/8 + gateway: 11.0.0.1 + metric: 3 +""" + class TestNetConfigParsing(TestCase): simple_cfg = { @@ -122,6 +281,29 @@ class TestNetConfigParsing(TestCase): self.assertEqual(found, self.simple_cfg) +class TestEniRoundTrip(TestCase): + def testsimple_convert_and_render(self): + network_config = net.convert_eni_data(EXAMPLE_ENI) + ns = net.parse_net_config_data(network_config) + eni = net.render_interfaces(ns) + print("Eni looks like:\n%s" % eni) + raise Exception("FOO") + + def testsimple_render_all(self): + network_config = yaml.load(NETWORK_YAML_ALL) + ns = net.parse_net_config_data(network_config) + eni = net.render_interfaces(ns) + print("Eni looks like:\n%s" % eni) + raise Exception("FOO") + + def testsimple_render_small(self): + network_config = yaml.load(NETWORK_YAML_SMALL) + ns = net.parse_net_config_data(network_config) + eni = net.render_interfaces(ns) + print("Eni looks like:\n%s" % eni) + raise Exception("FOO") + + def _gzip_data(data): with io.BytesIO() as iobuf: gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf) -- cgit v1.2.1 From 403a6045a84a9c9435f822dd7cc1baef33ad40ab Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 10:00:24 -0400 Subject: make _render_iface return a list of sections rather than one section --- cloudinit/net/eni.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 5a91fcf2..b93c8616 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -353,10 +353,11 @@ class Renderer(renderer.Renderer): return content def _render_iface(self, iface): - lines = [] + sections = [] subnets = iface.get('subnets', {}) if subnets: for index, subnet in zip(range(0, len(subnets)), subnets): + lines = [] iface['index'] = index iface['mode'] = subnet['type'] iface['control'] = subnet.get('control', 'auto') @@ -370,14 +371,17 @@ class Renderer(renderer.Renderer): lines.extend(_iface_start_entry(iface, index)) lines.extend(_iface_add_subnet(iface, subnet)) lines.extend(_iface_add_attrs(iface)) - lines.append("") + + sections.append(lines) else: # ifenslave docs say to auto the slave devices + lines = [] if 'bond-master' in iface: lines.append("auto {name}".format(**iface)) lines.append("iface {name} {inet} {mode}".format(**iface)) lines.extend(_iface_add_attrs(iface)) - return lines + sections.append(lines) + return sections def _render_interfaces(self, network_state): '''Given state, emit etc/network/interfaces content.''' @@ -411,20 +415,19 @@ class Renderer(renderer.Renderer): 'vlan': 3, } - sections = [self._render_iface(lo)] + sections = [] + sections.extend(self._render_iface(lo)) for iface in sorted(network_state.iter_interfaces(), key=lambda k: (order[k['type']], k['name'])): if iface.get('name') == "lo": continue - sections.append(self._render_iface(iface)) + sections.extend(self._render_iface(iface)) for route in network_state.iter_routes(): sections.append(self._render_route(route)) - # global replacements until v2 format - content = ''.join(['\n'.join(s) + '\n\n' for s in sections]) - return content + return '\n\n'.join(['\n'.join(s) for s in sections]) def render_network_state(self, target, network_state): fpeni = os.path.join(target, self.eni_path) -- cgit v1.2.1 From 87199f2812d4ccd3f9a9a5d9458543552f6552d2 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 10:06:38 -0400 Subject: minor cleanup --- cloudinit/net/eni.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index b93c8616..1fea240d 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -357,7 +357,6 @@ class Renderer(renderer.Renderer): subnets = iface.get('subnets', {}) if subnets: for index, subnet in zip(range(0, len(subnets)), subnets): - lines = [] iface['index'] = index iface['mode'] = subnet['type'] iface['control'] = subnet.get('control', 'auto') @@ -368,11 +367,11 @@ class Renderer(renderer.Renderer): if iface['mode'].startswith('dhcp'): iface['mode'] = 'dhcp' - lines.extend(_iface_start_entry(iface, index)) - lines.extend(_iface_add_subnet(iface, subnet)) - lines.extend(_iface_add_attrs(iface)) - - sections.append(lines) + sections.append( + _iface_start_entry(iface, index) + + _iface_add_subnet(iface, subnet) + + _iface_add_attrs(iface) + ) else: # ifenslave docs say to auto the slave devices lines = [] -- cgit v1.2.1 From 65d52719de2b088e5c28a037122cb6f6b479a58d Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 10:19:06 -0400 Subject: sort attributes in sections, change 'mac_address' to 'hwaddress' --- cloudinit/net/eni.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 1fea240d..bb48a6eb 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -64,7 +64,7 @@ def _iface_add_subnet(iface, subnet): key = key.replace('_', '-') content.append(" {} {}".format(key, value)) - return content + return sorted(content) # TODO: switch to valid_map for attrs @@ -80,16 +80,18 @@ def _iface_add_attrs(iface): 'subnets', 'type', ] + renames = {'mac_address': 'hwaddress'} if iface['type'] not in ['bond', 'bridge', 'vlan']: ignore_map.append('mac_address') for key, value in iface.items(): - if value and key not in ignore_map: - if type(value) == list: - value = " ".join(value) - content.append(" {} {}".format(key, value)) + if not value or key in ignore_map: + continue + if type(value) == list: + value = " ".join(value) + content.append(" {} {}".format(renames.get(key, key), value)) - return content + return sorted(content) def _iface_start_entry(iface, index): -- cgit v1.2.1 From 5aedb31c6af7f671b7e1bdfbb91768f063165e46 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 10:19:59 -0400 Subject: commit test changes cleaned up tests a bit. still they raise exception, but print out the files rendered and all use the _render_and_read helper. --- tests/unittests/test_net.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index a9268d30..ecd21b2b 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -505,29 +505,24 @@ class TestEniRoundTrip(TestCase): 'netrules_path': netrules_path}) renderer.render_network_state(self.tmp_dir, ns) + for f, c in dir2dict(self.tmp_dir).items(): + print("=== %s ===" % f) + print(c) return dir2dict(self.tmp_dir) def testsimple_convert_and_render(self): network_config = eni.convert_eni_data(EXAMPLE_ENI) - ns = network_state.parse_net_config_data(network_config) - eni_path = 'etc/network/interfaces.d/my.interfaces' - eni_full_path = os.path.join(self.tmp_dir, eni_path) - renderer = eni.Renderer(config={'eni_path': eni_path}) - renderer.render_network_state(self.tmp_dir, ns) - eni_content = util.load_file(eni_full_path) - print("Eni looks like: %s" % eni_content) + files = self._render_and_read(network_config=network_config) raise Exception("FOO1") def testsimple_render_all(self): - files = self._render_and_read(network_config=yaml.load(NETWORK_YAML_ALL)) - print("files: %s" % files) + files = self._render_and_read( + network_config=yaml.load(NETWORK_YAML_ALL)) raise Exception("FOO2") def skiptestsimple_render_small(self): - network_config = yaml.load(NETWORK_YAML_SMALL) - ns = network_state.parse_net_config_data(network_config) - eni = net.render_interfaces(ns) - print("Eni looks like:\n%s" % eni) + files = self._render_and_read( + network_config=yaml.load(NETWORK_YAML_SMALL)) raise Exception("FOO3") -- cgit v1.2.1 From 0e5a1b99615ea4953fc82d01d624a0fa8d91d91b Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 11:20:11 -0400 Subject: fix lost per-interface routes, use post-up to bring up interface aliases Fix the lack of per-interface routes, and add an example to yaml. in revno 394 in curtin, we added post-up for interface aliases. bring that commit here. --- cloudinit/net/eni.py | 11 ++++++++++- tests/unittests/test_net.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index bb48a6eb..2da13ffd 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -369,11 +369,20 @@ class Renderer(renderer.Renderer): if iface['mode'].startswith('dhcp'): iface['mode'] = 'dhcp' - sections.append( + lines = list( _iface_start_entry(iface, index) + _iface_add_subnet(iface, subnet) + _iface_add_attrs(iface) ) + for route in subnet.get('routes', []): + lines.extend(self._render_route(route, indent=" ")) + + if len(subnets) > 1 and index == 0: + tmpl = " post-up ifup %s:%s\n" + for i in range(1, len(subnets)): + lines.append(tmpl % (iface['name'], i)) + + sections.append(lines) else: # ifenslave docs say to auto the slave devices lines = [] diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index ecd21b2b..d50ac440 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -168,7 +168,7 @@ version: 1 config: # Physical interfaces. - type: physical - name: eth0 + name: eth99 mac_address: "c0:d6:9f:2c:e8:80" subnets: - type: dhcp4 @@ -178,6 +178,11 @@ config: - 8.8.8.8 - 8.8.4.4 dns_search: barley.maas sach.maas + routes: + - gateway: 65.61.151.37 + netmask: 0.0.0.0 + network: 0.0.0.0 + metric: 2 - type: physical name: eth1 mac_address: "cf:d6:af:48:e8:80" @@ -188,6 +193,7 @@ config: search: - wark.maas """ + NETWORK_YAML_ALL = """ version: 1 config: @@ -520,7 +526,7 @@ class TestEniRoundTrip(TestCase): network_config=yaml.load(NETWORK_YAML_ALL)) raise Exception("FOO2") - def skiptestsimple_render_small(self): + def testsimple_render_small(self): files = self._render_and_read( network_config=yaml.load(NETWORK_YAML_SMALL)) raise Exception("FOO3") -- cgit v1.2.1 From f54d504f31e8065a4e71d66701b2372dd67e271e Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 11:31:11 -0400 Subject: fix english in comment --- cloudinit/net/eni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 1c66319a..5f33e177 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -400,7 +400,7 @@ class Renderer(renderer.Renderer): content = "" # handle 'lo' specifically as we need to insert the global dns entries - # there (as that is the only interface) that will be always up. + # there (as that is the only interface that will be always up). lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet', 'subnets': [{'type': 'loopback', 'control': 'auto'}]} for iface in network_state.iter_interfaces(): -- cgit v1.2.1 From 1f120a8f6b818ae2d56dd7297f384e0a6cf1cf9b Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 12:26:02 -0400 Subject: net: fix inet value for subnets, don't add interface attributes to alias [copied from curtin revno 390] Apply two separate fixes for configuring bonding with ip aliases. Curtin re-used the interface's inet value for each subnet that might be configured. In the case where the configuration included an ipv4 address after an ipv6 one resulted in emitting 'inet6' for ipv4 address which is not correct. Resolve this issue by calculating the inet value independent of the current status of the iface, using the subnet config instead. When rendering a network_config which includes ip alias interfaces do not emit any attributes, like MTU, or bond/bridge options Including these values is almost always wrong or will result in confusing behavior on the target system. --- cloudinit/net/eni.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 5f33e177..86d2a830 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -68,8 +68,14 @@ def _iface_add_subnet(iface, subnet): # TODO: switch to valid_map for attrs - -def _iface_add_attrs(iface): +def _iface_add_attrs(iface, index): + # If the index is non-zero, this is an alias interface. Alias interfaces + # represent additional interface addresses, and should not have additional + # attributes. (extra attributes here are almost always either incorrect, + # or are applied to the parent interface.) So if this is an alias, stop + # right here. + if index != 0: + return [] content = [] ignore_map = [ 'control', @@ -363,17 +369,21 @@ class Renderer(renderer.Renderer): iface['index'] = index iface['mode'] = subnet['type'] iface['control'] = subnet.get('control', 'auto') + subnet_inet = 'inet' if iface['mode'].endswith('6'): - iface['inet'] += '6' + # This is a request for DHCPv6. + subnet_inet += '6' elif iface['mode'] == 'static' and ":" in subnet['address']: - iface['inet'] += '6' + # This is a static IPv6 address. + subnet_inet += '6' + iface['inet'] = subnet_inet if iface['mode'].startswith('dhcp'): iface['mode'] = 'dhcp' lines = list( _iface_start_entry(iface, index) + _iface_add_subnet(iface, subnet) + - _iface_add_attrs(iface) + _iface_add_attrs(iface, index) ) for route in subnet.get('routes', []): lines.extend(self._render_route(route, indent=" ")) @@ -390,7 +400,7 @@ class Renderer(renderer.Renderer): if 'bond-master' in iface: lines.append("auto {name}".format(**iface)) lines.append("iface {name} {inet} {mode}".format(**iface)) - lines.extend(_iface_add_attrs(iface)) + lines.extend(_iface_add_attrs(iface, index=0)) sections.append(lines) return sections -- cgit v1.2.1 From 07a21f2da9c4f909cd0badcbe99774a88c25ffdb Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 13:35:23 -0400 Subject: make 2 of 3 tests pass --- cloudinit/net/eni.py | 2 +- tests/unittests/test_net.py | 426 ++++++++++++++++++++++++++++---------------- 2 files changed, 275 insertions(+), 153 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 86d2a830..fe3800ed 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -448,7 +448,7 @@ class Renderer(renderer.Renderer): for route in network_state.iter_routes(): sections.append(self._render_route(route)) - return '\n\n'.join(['\n'.join(s) for s in sections]) + return '\n\n'.join(['\n'.join(s) for s in sections]) + "\n" def render_network_state(self, target, network_state): fpeni = os.path.join(target, self.eni_path) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index d50ac440..f47ae516 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -18,6 +18,7 @@ import json import os import shutil import tempfile +import textwrap import yaml DHCP_CONTENT_1 = """ @@ -163,148 +164,262 @@ iface eth1 inet static broadcast 10.248.2.7 """ -NETWORK_YAML_SMALL = """ -version: 1 -config: - # Physical interfaces. - - type: physical - name: eth99 - mac_address: "c0:d6:9f:2c:e8:80" - subnets: - - type: dhcp4 - - type: static - address: 192.168.21.3/24 - dns_nameservers: - - 8.8.8.8 - - 8.8.4.4 - dns_search: barley.maas sach.maas - routes: - - gateway: 65.61.151.37 - netmask: 0.0.0.0 - network: 0.0.0.0 - metric: 2 - - type: physical - name: eth1 - mac_address: "cf:d6:af:48:e8:80" - - type: nameserver - address: - - 1.2.3.4 - - 5.6.7.8 - search: - - wark.maas -""" +RENDERED_ENI = """ +auto lo +iface lo inet loopback + dns-nameservers 10.0.0.1 + dns-search foo.com -NETWORK_YAML_ALL = """ -version: 1 -config: - # Physical interfaces. - - type: physical - name: eth0 - mac_address: "c0:d6:9f:2c:e8:80" - - type: physical - name: eth1 - mac_address: "aa:d6:9f:2c:e8:80" - - type: physical - name: eth2 - mac_address: "c0:bb:9f:2c:e8:80" - - type: physical - name: eth3 - mac_address: "66:bb:9f:2c:e8:80" - - type: physical - name: eth4 - mac_address: "98:bb:9f:2c:e8:80" - # specify how ifupdown should treat iface - # control is one of ['auto', 'hotplug', 'manual'] - # with manual meaning ifup/ifdown should not affect the iface - # useful for things like iscsi root + dhcp - - type: physical - name: eth5 - mac_address: "98:bb:9f:2c:e8:8a" - subnets: - - type: dhcp - control: manual - # VLAN interface. - - type: vlan - name: eth0.101 - vlan_link: eth0 - vlan_id: 101 - mtu: 1500 - subnets: - - type: static - address: 192.168.0.2/24 - gateway: 192.168.0.1 - dns_nameservers: - - 192.168.0.10 - - 10.23.23.134 - dns_search: - - barley.maas - - sacchromyces.maas - - brettanomyces.maas - - type: static - address: 192.168.2.10/24 - # Bond. - - type: bond - name: bond0 - # if 'mac_address' is omitted, the MAC is taken from - # the first slave. - mac_address: "aa:bb:cc:dd:ee:ff" - bond_interfaces: - - eth1 - - eth2 - params: - bond-mode: active-backup - subnets: - - type: dhcp6 - # A Bond VLAN. - - type: vlan - name: bond0.200 - vlan_link: bond0 - vlan_id: 200 - subnets: - - type: dhcp4 - # A bridge. - - type: bridge - name: br0 - bridge_interfaces: - - eth3 - - eth4 - ipv4_conf: - rp_filter: 1 - proxy_arp: 0 - forwarding: 1 - ipv6_conf: - autoconf: 1 - disable_ipv6: 1 - use_tempaddr: 1 - forwarding: 1 - # basically anything in /proc/sys/net/ipv6/conf/.../ - params: - bridge_stp: 'off' - bridge_fd: 0 - bridge_maxwait: 0 - subnets: - - type: static - address: 192.168.14.2/24 - - type: static - address: 2001:1::1/64 # default to /64 - # A global nameserver. - - type: nameserver - address: 8.8.8.8 - search: barley.maas - # global nameservers and search in list form - - type: nameserver - address: - - 4.4.4.4 - - 8.8.4.4 - search: - - wark.maas - - foobar.maas - # A global route. - - type: route - destination: 10.0.0.0/8 - gateway: 11.0.0.1 - metric: 3 -""" +auto eth0 +iface eth0 inet static + address 1.2.3.12 + broadcast 1.2.3.15 + dns-nameservers 69.9.160.191 69.9.191.4 + gateway 1.2.3.9 + netmask 255.255.255.248 + +auto eth1 +iface eth1 inet static + address 10.248.2.4 + broadcast 10.248.2.7 + netmask 255.255.255.248 +""".lstrip() + +NETWORK_CONFIGS = { + 'small': { + 'expected_eni': textwrap.dedent("""\ + auto lo + iface lo inet loopback + dns-nameservers 1.2.3.4 5.6.7.8 + dns-search wark.maas + + iface eth1 inet manual + + auto eth99 + iface eth99 inet dhcp + post-up ifup eth99:1 + + + auto eth99:1 + iface eth99:1 inet static + address 192.168.21.3/24 + dns-nameservers 8.8.8.8 8.8.4.4 + dns-search barley.maas sach.maas + post-up route add default gw 65.61.151.37 || true + pre-down route del default gw 65.61.151.37 || true + """).rstrip(' '), + 'yaml': textwrap.dedent(""" + version: 1 + config: + # Physical interfaces. + - type: physical + name: eth99 + mac_address: "c0:d6:9f:2c:e8:80" + subnets: + - type: dhcp4 + - type: static + address: 192.168.21.3/24 + dns_nameservers: + - 8.8.8.8 + - 8.8.4.4 + dns_search: barley.maas sach.maas + routes: + - gateway: 65.61.151.37 + netmask: 0.0.0.0 + network: 0.0.0.0 + metric: 2 + - type: physical + name: eth1 + mac_address: "cf:d6:af:48:e8:80" + - type: nameserver + address: + - 1.2.3.4 + - 5.6.7.8 + search: + - wark.maas + """), + }, + 'all': { + 'expected_eni': ("""\ +auto lo +iface lo inet loopback + dns-nameservers 8.8.8.8 4.4.4.4 8.8.4.4 + dns-search barley.maas wark.maas foobar.maas + +iface eth0 inet manual + +auto eth1 +iface eth1 inet manual + bond-master bond0 + bond-mode active-backup + +auto eth2 +iface eth2 inet manual + bond-master bond0 + bond-mode active-backup + +iface eth3 inet manual + +iface eth4 inet manual + +# control-manual eth5 +iface eth5 inet dhcp + +auto bond0 +iface bond0 inet6 dhcp + bond-mode active-backup + bond-slaves none + hwaddress aa:bb:cc:dd:ee:ff + +auto br0 +iface br0 inet static + address 192.168.14.2/24 + bridge_ports eth3 eth4 + bridge_stp off + post-up ifup br0:1 + + +auto br0:1 +iface br0:1 inet6 static + address 2001:1::1/64 + +auto bond0.200 +iface bond0.200 inet dhcp + vlan-raw-device bond0 + vlan_id 200 + +auto eth0.101 +iface eth0.101 inet static + address 192.168.0.2/24 + dns-nameservers 192.168.0.10 10.23.23.134 + dns-search barley.maas sacchromyces.maas brettanomyces.maas + gateway 192.168.0.1 + mtu 1500 + vlan-raw-device eth0 + vlan_id 101 + post-up ifup eth0.101:1 + + +auto eth0.101:1 +iface eth0.101:1 inet static + address 192.168.2.10/24 + +post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true +pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true +"""), + 'yaml': textwrap.dedent(""" + version: 1 + config: + # Physical interfaces. + - type: physical + name: eth0 + mac_address: "c0:d6:9f:2c:e8:80" + - type: physical + name: eth1 + mac_address: "aa:d6:9f:2c:e8:80" + - type: physical + name: eth2 + mac_address: "c0:bb:9f:2c:e8:80" + - type: physical + name: eth3 + mac_address: "66:bb:9f:2c:e8:80" + - type: physical + name: eth4 + mac_address: "98:bb:9f:2c:e8:80" + # specify how ifupdown should treat iface + # control is one of ['auto', 'hotplug', 'manual'] + # with manual meaning ifup/ifdown should not affect the iface + # useful for things like iscsi root + dhcp + - type: physical + name: eth5 + mac_address: "98:bb:9f:2c:e8:8a" + subnets: + - type: dhcp + control: manual + # VLAN interface. + - type: vlan + name: eth0.101 + vlan_link: eth0 + vlan_id: 101 + mtu: 1500 + subnets: + - type: static + address: 192.168.0.2/24 + gateway: 192.168.0.1 + dns_nameservers: + - 192.168.0.10 + - 10.23.23.134 + dns_search: + - barley.maas + - sacchromyces.maas + - brettanomyces.maas + - type: static + address: 192.168.2.10/24 + # Bond. + - type: bond + name: bond0 + # if 'mac_address' is omitted, the MAC is taken from + # the first slave. + mac_address: "aa:bb:cc:dd:ee:ff" + bond_interfaces: + - eth1 + - eth2 + params: + bond-mode: active-backup + subnets: + - type: dhcp6 + # A Bond VLAN. + - type: vlan + name: bond0.200 + vlan_link: bond0 + vlan_id: 200 + subnets: + - type: dhcp4 + # A bridge. + - type: bridge + name: br0 + bridge_interfaces: + - eth3 + - eth4 + ipv4_conf: + rp_filter: 1 + proxy_arp: 0 + forwarding: 1 + ipv6_conf: + autoconf: 1 + disable_ipv6: 1 + use_tempaddr: 1 + forwarding: 1 + # basically anything in /proc/sys/net/ipv6/conf/.../ + params: + bridge_stp: 'off' + bridge_fd: 0 + bridge_maxwait: 0 + subnets: + - type: static + address: 192.168.14.2/24 + - type: static + address: 2001:1::1/64 # default to /64 + # A global nameserver. + - type: nameserver + address: 8.8.8.8 + search: barley.maas + # global nameservers and search in list form + - type: nameserver + address: + - 4.4.4.4 + - 8.8.4.4 + search: + - wark.maas + - foobar.maas + # A global route. + - type: route + destination: 10.0.0.0/8 + gateway: 11.0.0.1 + metric: 3 + """).lstrip(), + } +} def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info, @@ -502,7 +617,7 @@ class TestEniRoundTrip(TestCase): ns = state else: raise ValueError("Expected data or state, got neither") - + if eni_path is None: eni_path = 'etc/network/interfaces' @@ -511,25 +626,32 @@ class TestEniRoundTrip(TestCase): 'netrules_path': netrules_path}) renderer.render_network_state(self.tmp_dir, ns) - for f, c in dir2dict(self.tmp_dir).items(): - print("=== %s ===" % f) - print(c) + #for f, c in dir2dict(self.tmp_dir).items(): + # print("=== %s ===" % f) + # print(c) return dir2dict(self.tmp_dir) def testsimple_convert_and_render(self): network_config = eni.convert_eni_data(EXAMPLE_ENI) files = self._render_and_read(network_config=network_config) - raise Exception("FOO1") + self.assertEqual( + RENDERED_ENI.splitlines(), + files['/etc/network/interfaces'].splitlines()) def testsimple_render_all(self): - files = self._render_and_read( - network_config=yaml.load(NETWORK_YAML_ALL)) - raise Exception("FOO2") + entry = NETWORK_CONFIGS['all'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + for f in files: print(f) + self.assertEqual( + entry['expected_eni'].splitlines(), + files['/etc/network/interfaces'].splitlines()) def testsimple_render_small(self): - files = self._render_and_read( - network_config=yaml.load(NETWORK_YAML_SMALL)) - raise Exception("FOO3") + entry = NETWORK_CONFIGS['small'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + self.assertEqual( + entry['expected_eni'].splitlines(), + files['/etc/network/interfaces'].splitlines()) def _gzip_data(data): -- cgit v1.2.1 From a1b4d0d59e05b227f5dd2245ebd0e78c381a6e44 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 13:36:30 -0400 Subject: fix flake8 --- cloudinit/net/eni.py | 4 +--- tests/unittests/test_net.py | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index fe3800ed..d963d1f2 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -379,7 +379,7 @@ class Renderer(renderer.Renderer): iface['inet'] = subnet_inet if iface['mode'].startswith('dhcp'): iface['mode'] = 'dhcp' - + lines = list( _iface_start_entry(iface, index) + _iface_add_subnet(iface, subnet) + @@ -407,8 +407,6 @@ class Renderer(renderer.Renderer): def _render_interfaces(self, network_state): '''Given state, emit etc/network/interfaces content.''' - content = "" - # handle 'lo' specifically as we need to insert the global dns entries # there (as that is the only interface that will be always up). lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet', diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index f47ae516..f39fd97a 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -6,9 +6,9 @@ from cloudinit.net import sysconfig from cloudinit.sources.helpers import openstack from cloudinit import util +from .helpers import dir2dict from .helpers import mock from .helpers import TestCase -from .helpers import dir2dict import base64 import copy @@ -626,9 +626,6 @@ class TestEniRoundTrip(TestCase): 'netrules_path': netrules_path}) renderer.render_network_state(self.tmp_dir, ns) - #for f, c in dir2dict(self.tmp_dir).items(): - # print("=== %s ===" % f) - # print(c) return dir2dict(self.tmp_dir) def testsimple_convert_and_render(self): @@ -641,7 +638,6 @@ class TestEniRoundTrip(TestCase): def testsimple_render_all(self): entry = NETWORK_CONFIGS['all'] files = self._render_and_read(network_config=yaml.load(entry['yaml'])) - for f in files: print(f) self.assertEqual( entry['expected_eni'].splitlines(), files['/etc/network/interfaces'].splitlines()) -- cgit v1.2.1 From 0a56e98326ea754a09c3f35339bba01dd8e893e2 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 21 Jun 2016 13:43:48 -0400 Subject: no longer skip 'lo' elements when reading. pass 3rd test. --- cloudinit/net/eni.py | 4 ---- tests/unittests/test_net.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index d963d1f2..1383dd6b 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -271,10 +271,6 @@ def _ifaces_to_net_config_data(ifaces): for name, data in ifaces.items(): # devname is 'eth0' for name='eth0:1' devname = name.partition(":")[0] - if devname == "lo": - # currently provding 'lo' in network config results in duplicate - # entries. in rendered interfaces file. so skip it. - continue if devname not in devs: devs[devname] = {'type': 'physical', 'name': devname, 'subnets': []} diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index f39fd97a..ee9061a9 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -167,8 +167,8 @@ iface eth1 inet static RENDERED_ENI = """ auto lo iface lo inet loopback - dns-nameservers 10.0.0.1 - dns-search foo.com + dns-nameservers 10.0.0.1 + dns-search foo.com auto eth0 iface eth0 inet static -- cgit v1.2.1