From fe6919dcd37c6c1ecd371e5eb20b605ab20a6420 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.3 From 43de8eb4c58aa18518c33e811202b9b322ddfb3c 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.3 From d818443bec7fe9c9d5c880f47303b19ce2bc398f 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.3 From 9c006ca13981dcfb01324dbadacda741d5610401 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.3 From f21f3e262493b9f2f07710e51e7f0247e5132a22 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.3 From 575c7030e4ff1bd68ca7229f10c482af00620711 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.3 From 5f0634905cae5c754161733be8f408763971938c 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.3 From 9633891a8f6dbc4a96eb1ad58834356736133ebb 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. LP: #1588547 --- 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.3 From ed28818c80628b10c2435e77b86c81dad05674e1 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.3 From 58d0b39d20ec5c18b1c9449c6358242860356e0d 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.3 From 2a8dc75a5b8ab42ca805f2ec6b97dc7a074e82f3 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.3 From b3193a4b5204d12825d8317bf0f8c3577c1d8153 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 28 Jun 2016 11:52:09 -0400 Subject: distros/debian.py: fix calling of eni renderer to not render link files Under revno 1243 a failed attempt was made to not render systemd.link files into /etc/systemd/network/ . The 'config' that was passed in was incorrect though, and resulted in link files still getting rendered. (original bug was LP: #1594546). --- cloudinit/distros/debian.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 5ae9a509..f9b3b92e 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -55,7 +55,6 @@ class Distro(distros.Distro): hostname_conf_fn = "/etc/hostname" locale_conf_fn = "/etc/default/locale" network_conf_fn = "/etc/network/interfaces.d/50-cloud-init.cfg" - links_prefix = "/etc/systemd/network/50-cloud-init-" def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) @@ -67,7 +66,7 @@ class Distro(distros.Distro): self._net_renderer = eni.Renderer({ 'eni_path': self.network_conf_fn, 'eni_header': ENI_HEADER, - 'links_prefix_path': None, + 'links_path_prefix': None, 'netrules_path': None, }) -- cgit v1.2.3 From fc203087c9e5ca11005ecab6191c9125b046bf26 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 30 Jun 2016 15:59:25 -0700 Subject: Fixs missing/unpacked rpm files These new files were not getting picked up during packaging (and they need to, otherwise rpm building fails). - 66-azure-ephemeral.rules - cloud-init-generator --- packages/redhat/cloud-init.spec.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in index 81cccce5..254d209b 100644 --- a/packages/redhat/cloud-init.spec.in +++ b/packages/redhat/cloud-init.spec.in @@ -160,6 +160,8 @@ fi %files +/lib/udev/rules.d/66-azure-ephemeral.rules + #if $sysvinit %attr(0755, root, root) %{_initddir}/cloud-config %attr(0755, root, root) %{_initddir}/cloud-final @@ -168,6 +170,8 @@ fi #end if #if $systemd +/usr/lib/systemd/system-generators/cloud-init-generator +%{_unitdir}/cloud-* %{_unitdir}/cloud-* #end if -- cgit v1.2.3 From 504d17f95719e9501d8a55f95ef16f0467206083 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 12 Jul 2016 17:17:41 -0700 Subject: Dict comprehensions don't work in 2.6 --- cloudinit/net/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index 63e54f91..21cc602b 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -252,7 +252,8 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, cur_bymac[mac] = cur def update_byname(bymac): - return {data['name']: data for data in bymac.values()} + return dict((data['name'], data) + for data in bymac.values()) def rename(cur, new): util.subp(["ip", "link", "set", cur, "name", new], capture=True) -- cgit v1.2.3 From 78a85240aac5b252bae4108514681bed7340c034 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 13 Jul 2016 14:54:56 -0700 Subject: Remove another stray dict comprehension --- cloudinit/sources/helpers/openstack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py index d52cb56a..2d001061 100644 --- a/cloudinit/sources/helpers/openstack.py +++ b/cloudinit/sources/helpers/openstack.py @@ -542,8 +542,8 @@ def convert_net_json(network_json=None, known_macs=None): config = [] for link in links: subnets = [] - cfg = {k: v for k, v in link.items() - if k in valid_keys['physical']} + cfg = dict((k, v) for k, v in link.items() + if k in valid_keys['physical']) # 'name' is not in openstack spec yet, but we will support it if it is # present. The 'id' in the spec is currently implemented as the host # nic's name, meaning something like 'tap-adfasdffd'. We do not want -- cgit v1.2.3 From f5ea273e36615cfbbe8f35b3963935e75e22c4a1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 13 Jul 2016 15:04:07 -0700 Subject: Another stray occurence of a dict comprehension being removed --- cloudinit/sources/helpers/openstack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py index 2d001061..2e7a1d47 100644 --- a/cloudinit/sources/helpers/openstack.py +++ b/cloudinit/sources/helpers/openstack.py @@ -553,8 +553,8 @@ def convert_net_json(network_json=None, known_macs=None): for network in [n for n in networks if n['link'] == link['id']]: - subnet = {k: v for k, v in network.items() - if k in valid_keys['subnet']} + subnet = dict((k, v) for k, v in network.items() + if k in valid_keys['subnet']) if 'dhcp' in network['type']: t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4' subnet.update({ -- cgit v1.2.3 From ba55775fbd18452d79d5962c912995ac36567dc2 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 13 Jul 2016 15:10:54 -0700 Subject: String format requires positions on python 2.6 --- cloudinit/net/eni.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index e5ed10fd..91f83e60 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -61,7 +61,7 @@ def _iface_add_subnet(iface, subnet): value = " ".join(value) if '_' in key: key = key.replace('_', '-') - content += " {} {}\n".format(key, value) + content += " {0} {1}\n".format(key, value) return content @@ -86,7 +86,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 += " {0} {1}\n".format(key, value) return content -- cgit v1.2.3 From 333eea5105956c978043579a49d935af6d3ceff2 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 13 Jul 2016 15:18:46 -0700 Subject: Fix SmartOS datasource usage of dict comprehensions --- cloudinit/sources/DataSourceSmartOS.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 08bc132b..ccc86883 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -718,8 +718,8 @@ def convert_smartos_network_data(network_data=None): config = [] for nic in network_data: - cfg = {k: v for k, v in nic.items() - if k in valid_keys['physical']} + cfg = dict((k, v) for k, v in nic.items() + if k in valid_keys['physical']) cfg.update({ 'type': 'physical', 'name': nic['interface']}) @@ -728,8 +728,8 @@ def convert_smartos_network_data(network_data=None): subnets = [] for ip, gw in zip(nic['ips'], nic['gateways']): - subnet = {k: v for k, v in nic.items() - if k in valid_keys['subnet']} + subnet = dict((k, v) for k, v in nic.items() + if k in valid_keys['subnet']) subnet.update({ 'type': 'static', 'address': ip, -- cgit v1.2.3 From efef1c263ab1c473fd90f3782a785edab7d02430 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Jul 2016 22:03:42 -0400 Subject: ConfigDrive: write 'injected' files and legacy networking Previous commit disabled the consumption of 'injected' files in configdrive (openstack server boot --file=/target/file=local-file) unless the datasource was in 'pass' mode. The default mode is 'net' so that would never happen. Also here are: a.) a fix for 'links_path_prefix' string from debian, to finally disable the rendering of systemd.link files (LP: #1594546) b.) some comments to apply_network_config c.) implement a backwards compatibility for for distros that do not yet implement apply_network_config by converting the network config into ENI format and calling apply_network. This is required because prior to the previous commit, those distros would have had 'apply_network' called with the openstack provided ENI file. But after this change they will have apply_network_config called by cloudinit's main. d.) a network_state_to_eni helper for converting net config to eni it supports the not-actually-correct 'hwaddress' field in ENI. LP: #1602373 --- cloudinit/distros/__init__.py | 27 ++++++++++++++++++++++++-- cloudinit/distros/debian.py | 2 +- cloudinit/net/eni.py | 24 ++++++++++++++++++++--- cloudinit/sources/DataSourceConfigDrive.py | 26 ++++++++++++++++--------- tests/unittests/test_net.py | 31 ++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 15 deletions(-) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 14b500f8..006a7062 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -32,6 +32,8 @@ import stat from cloudinit import importer from cloudinit import log as logging from cloudinit import net +from cloudinit.net import eni +from cloudinit.net import network_state from cloudinit import ssh_util from cloudinit import type_utils from cloudinit import util @@ -138,9 +140,30 @@ class Distro(object): return self._bring_up_interfaces(dev_names) return False + def _apply_network_from_network_config(self, netconfig, bring_up=True): + LOG.warn("apply_network_config is not currently implemented " + "for distribution '%s'. Attempting to use " + "apply_network", self.__class__) + header = '\n'.join([ + "# Converted from network_config for distro %s" % self.__class__, + "# Implmentation of _write_network_config is needed." + ]) + ns = network_state.parse_net_config_data(netconfig) + contents = eni.network_state_to_eni( + ns, header=header, render_hwaddress=True) + self.apply_network(contents, bring_up=bring_up) + def apply_network_config(self, netconfig, bring_up=False): - # Write it out - dev_names = self._write_network_config(netconfig) + # apply network config netconfig + # This method is preferred to apply_network which only takes + # a much less complete network config format (interfaces(5)). + try: + dev_names = self._write_network_config(netconfig) + except NotImplementedError: + # backwards compat until all distros have apply_network_config + return self._apply_network_from_network_config( + netconfig, bring_up=bring_up) + # Now try to bring them up if bring_up: return self._bring_up_interfaces(dev_names) diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 5ae9a509..4d336b5b 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -67,7 +67,7 @@ class Distro(distros.Distro): self._net_renderer = eni.Renderer({ 'eni_path': self.network_conf_fn, 'eni_header': ENI_HEADER, - 'links_prefix_path': None, + 'links_path_prefix': None, 'netrules_path': None, }) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index e5ed10fd..419e7a74 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -352,7 +352,7 @@ class Renderer(renderer.Renderer): content += down + route_line + eol return content - def _render_interfaces(self, network_state): + def _render_interfaces(self, network_state, render_hwaddress=False): '''Given state, emit etc/network/interfaces content.''' content = "" @@ -397,6 +397,8 @@ class Renderer(renderer.Renderer): iface['mode'] = 'dhcp' content += _iface_start_entry(iface, index) + if render_hwaddress and iface.get('mac_address'): + content += " hwaddress %s" % iface['mac_address'] content += _iface_add_subnet(iface, subnet) content += _iface_add_attrs(iface) for route in subnet.get('routes', []): @@ -411,8 +413,6 @@ class Renderer(renderer.Renderer): for route in network_state.iter_routes(): content += self._render_route(route) - # global replacements until v2 format - content = content.replace('mac_address', 'hwaddress') return content def render_network_state(self, target, network_state): @@ -448,3 +448,21 @@ class Renderer(renderer.Renderer): "" ]) util.write_file(fname, content) + + +def network_state_to_eni(network_state, header=None, render_hwaddress=False): + # render the provided network state, return a string of equivalent eni + eni_path = 'etc/network/interfaces' + renderer = Renderer({ + 'eni_path': eni_path, + 'eni_header': header, + 'links_path_prefix': None, + 'netrules_path': None, + }) + if not header: + header = "" + if not header.endswith("\n"): + header += "\n" + contents = renderer._render_interfaces( + network_state, render_hwaddress=render_hwaddress) + return header + contents diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index 3130e618..91d6ff13 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -107,12 +107,19 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource): if self.dsmode == sources.DSMODE_DISABLED: return False - # This is legacy and sneaky. If dsmode is 'pass' then write - # 'injected files' and apply legacy ENI network format. prev_iid = get_previous_iid(self.paths) cur_iid = md['instance-id'] - if prev_iid != cur_iid and self.dsmode == sources.DSMODE_PASS: - on_first_boot(results, distro=self.distro) + if prev_iid != cur_iid: + # better would be to handle this centrally, allowing + # the datasource to do something on new instance id + # note, networking is only rendered here if dsmode is DSMODE_PASS + # which means "DISABLED, but render files and networking" + on_first_boot(results, distro=self.distro, + network=self.dsmode == sources.DSMODE_PASS) + + # This is legacy and sneaky. If dsmode is 'pass' then do not claim + # the datasource was used, even though we did run on_first_boot above. + if self.dsmode == sources.DSMODE_PASS: LOG.debug("%s: not claiming datasource, dsmode=%s", self, self.dsmode) return False @@ -184,15 +191,16 @@ def get_previous_iid(paths): return None -def on_first_boot(data, distro=None): +def on_first_boot(data, distro=None, network=True): """Performs any first-boot actions using data read from a config-drive.""" if not isinstance(data, dict): raise TypeError("Config-drive data expected to be a dict; not %s" % (type(data))) - net_conf = data.get("network_config", '') - if net_conf and distro: - LOG.warn("Updating network interfaces from config drive") - distro.apply_network(net_conf) + if network: + net_conf = data.get("network_config", '') + if net_conf and distro: + LOG.warn("Updating network interfaces from config drive") + distro.apply_network(net_conf) write_injected_files(data.get('files')) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 3ae00fc6..6f4dad13 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -269,6 +269,37 @@ iface eth1000 inet dhcp self.assertEqual(expected.lstrip(), contents.lstrip()) +class TestEniNetworkStateToEni(TestCase): + mycfg = { + 'config': [{"type": "physical", "name": "eth0", + "mac_address": "c0:d6:9f:2c:e8:80", + "subnets": [{"type": "dhcp"}]}], + 'version': 1} + my_mac = 'c0:d6:9f:2c:e8:80' + + def test_no_header(self): + rendered = eni.network_state_to_eni( + network_state=network_state.parse_net_config_data(self.mycfg), + render_hwaddress=True) + self.assertIn(self.my_mac, rendered) + self.assertIn("hwaddress", rendered) + + def test_with_header(self): + header = "# hello world\n" + rendered = eni.network_state_to_eni( + network_state=network_state.parse_net_config_data(self.mycfg), + header=header, render_hwaddress=True) + self.assertIn(header, rendered) + self.assertIn(self.my_mac, rendered) + + def test_no_hwaddress(self): + rendered = eni.network_state_to_eni( + network_state=network_state.parse_net_config_data(self.mycfg), + render_hwaddress=False) + self.assertNotIn(self.my_mac, rendered) + self.assertNotIn("hwaddress", rendered) + + class TestCmdlineConfigParsing(TestCase): simple_cfg = { 'config': [{"type": "physical", "name": "eth0", -- cgit v1.2.3 From 7d58c949c2f5d7821ec664ed25167dfea92964f7 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 13 Jul 2016 22:10:22 -0400 Subject: pass the return back up, shorten lines some. --- cloudinit/distros/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 006a7062..40af8802 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -141,17 +141,18 @@ class Distro(object): return False def _apply_network_from_network_config(self, netconfig, bring_up=True): + distro = self.__class__ LOG.warn("apply_network_config is not currently implemented " - "for distribution '%s'. Attempting to use " - "apply_network", self.__class__) + "for distribution '%s'. Attempting to use apply_network", + distro) header = '\n'.join([ - "# Converted from network_config for distro %s" % self.__class__, + "# Converted from network_config for distro %s" % distro, "# Implmentation of _write_network_config is needed." ]) ns = network_state.parse_net_config_data(netconfig) contents = eni.network_state_to_eni( ns, header=header, render_hwaddress=True) - self.apply_network(contents, bring_up=bring_up) + return self.apply_network(contents, bring_up=bring_up) def apply_network_config(self, netconfig, bring_up=False): # apply network config netconfig -- cgit v1.2.3 From 200e811268b7c17e814cfb6b7ef3f603c0590abc Mon Sep 17 00:00:00 2001 From: Sergii Golovatiuk Date: Thu, 14 Jul 2016 16:58:56 +0200 Subject: Change StringIO to BytesIO in cc_mcollective.py * StringIO from six doesn't act as 'binary stream' in Python 3. This patch changes StringIO to BytesIO to have code compatible with Python 3 and Python 2. * Add try/except for IOError in case when server.cfg doesn't exists. This is necessary for unit tests or cases when server.cfg is not included to package * Add UnitTest for cc_mcollective.py LP: #1597699 --- cloudinit/config/cc_mcollective.py | 72 +++++++++++++--------- .../test_handler/test_handler_mcollective.py | 60 ++++++++++++++++++ 2 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 tests/unittests/test_handler/test_handler_mcollective.py diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py index 425420ae..47320d5d 100644 --- a/cloudinit/config/cc_mcollective.py +++ b/cloudinit/config/cc_mcollective.py @@ -20,54 +20,49 @@ # along with this program. If not, see . import six -from six import StringIO +from six import BytesIO # Used since this can maintain comments # and doesn't need a top level section from configobj import ConfigObj from cloudinit import util +from cloudinit import log as logging PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem" PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem" SERVER_CFG = '/etc/mcollective/server.cfg' +LOG = logging.getLogger(__name__) -def handle(name, cfg, cloud, log, _args): - - # If there isn't a mcollective key in the configuration don't do anything - if 'mcollective' not in cfg: - log.debug(("Skipping module named %s, " - "no 'mcollective' key in configuration"), name) - return - - mcollective_cfg = cfg['mcollective'] - - # Start by installing the mcollective package ... - cloud.distro.install_packages(("mcollective",)) - # ... and then update the mcollective configuration - if 'conf' in mcollective_cfg: - # Read server.cfg values from the - # original file in order to be able to mix the rest up - mcollective_config = ConfigObj(SERVER_CFG) - # See: http://tiny.cc/jh9agw - for (cfg_name, cfg) in mcollective_cfg['conf'].items(): +def configure(config): + # Read server.cfg values from the + # original file in order to be able to mix the rest up + try: + mcollective_config = ConfigObj(SERVER_CFG, file_error=True) + except IOError: + LOG.warn("Did not find file %s", SERVER_CFG) + mcollective_config = ConfigObj(config) + else: + for (cfg_name, cfg) in config.items(): if cfg_name == 'public-cert': util.write_file(PUBCERT_FILE, cfg, mode=0o644) - mcollective_config['plugin.ssl_server_public'] = PUBCERT_FILE + mcollective_config[ + 'plugin.ssl_server_public'] = PUBCERT_FILE mcollective_config['securityprovider'] = 'ssl' elif cfg_name == 'private-cert': util.write_file(PRICERT_FILE, cfg, mode=0o600) - mcollective_config['plugin.ssl_server_private'] = PRICERT_FILE + mcollective_config[ + 'plugin.ssl_server_private'] = PRICERT_FILE mcollective_config['securityprovider'] = 'ssl' else: if isinstance(cfg, six.string_types): # Just set it in the 'main' section mcollective_config[cfg_name] = cfg elif isinstance(cfg, (dict)): - # Iterate through the config items, create a section - # if it is needed and then add/or create items as needed + # Iterate through the config items, create a section if + # it is needed and then add/or create items as needed if cfg_name not in mcollective_config.sections: mcollective_config[cfg_name] = {} for (o, v) in cfg.items(): @@ -78,11 +73,30 @@ def handle(name, cfg, cloud, log, _args): # We got all our config as wanted we'll rename # the previous server.cfg and create our new one util.rename(SERVER_CFG, "%s.old" % (SERVER_CFG)) - # Now we got the whole file, write to disk... - contents = StringIO() - mcollective_config.write(contents) - contents = contents.getvalue() - util.write_file(SERVER_CFG, contents, mode=0o644) + + # Now we got the whole file, write to disk... + contents = BytesIO() + mcollective_config.write(contents) + contents = contents.getvalue() + util.write_file(SERVER_CFG, contents, mode=0o644) + + +def handle(name, cfg, cloud, log, _args): + + # If there isn't a mcollective key in the configuration don't do anything + if 'mcollective' not in cfg: + log.debug(("Skipping module named %s, " + "no 'mcollective' key in configuration"), name) + return + + mcollective_cfg = cfg['mcollective'] + + # Start by installing the mcollective package ... + cloud.distro.install_packages(("mcollective",)) + + # ... and then update the mcollective configuration + if 'conf' in mcollective_cfg: + configure(config=mcollective_cfg['conf']) # Start mcollective util.subp(['service', 'mcollective', 'start'], capture=False) diff --git a/tests/unittests/test_handler/test_handler_mcollective.py b/tests/unittests/test_handler/test_handler_mcollective.py new file mode 100644 index 00000000..0e3fcc8c --- /dev/null +++ b/tests/unittests/test_handler/test_handler_mcollective.py @@ -0,0 +1,60 @@ +from cloudinit.config import cc_mcollective +from cloudinit import util + +from .. import helpers + +import configobj +import logging +import shutil +from six import BytesIO +import tempfile + +LOG = logging.getLogger(__name__) + + +class TestConfig(helpers.FilesystemMockingTestCase): + def setUp(self): + super(TestConfig, self).setUp() + self.tmp = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tmp) + + def test_basic_config(self): + cfg = { + 'mcollective': { + 'conf': { + 'loglevel': 'debug', + 'connector': 'rabbitmq', + 'logfile': '/var/log/mcollective.log', + 'ttl': '4294957', + 'collectives': 'mcollective', + 'main_collective': 'mcollective', + 'securityprovider': 'psk', + 'daemonize': '1', + 'factsource': 'yaml', + 'direct_addressing': '1', + 'plugin.psk': 'unset', + 'libdir': '/usr/share/mcollective/plugins', + 'identity': '1', + }, + }, + } + self.patchUtils(self.tmp) + cc_mcollective.configure(cfg['mcollective']['conf']) + contents = util.load_file("/etc/mcollective/server.cfg", decode=False) + contents = configobj.ConfigObj(BytesIO(contents)) + expected = { + 'loglevel': 'debug', + 'connector': 'rabbitmq', + 'logfile': '/var/log/mcollective.log', + 'ttl': '4294957', + 'collectives': 'mcollective', + 'main_collective': 'mcollective', + 'securityprovider': 'psk', + 'daemonize': '1', + 'factsource': 'yaml', + 'direct_addressing': '1', + 'plugin.psk': 'unset', + 'libdir': '/usr/share/mcollective/plugins', + 'identity': '1', + } + self.assertEqual(expected, dict(contents)) -- cgit v1.2.3 From 74dcabe8853e063b63be578dff7926f62bd26df8 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Jul 2016 11:28:46 -0400 Subject: fix tox flake8 --- cloudinit/config/cc_mcollective.py | 2 +- .../test_handler/test_handler_mcollective.py | 28 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py index 47320d5d..0c84d600 100644 --- a/cloudinit/config/cc_mcollective.py +++ b/cloudinit/config/cc_mcollective.py @@ -26,8 +26,8 @@ from six import BytesIO # and doesn't need a top level section from configobj import ConfigObj -from cloudinit import util from cloudinit import log as logging +from cloudinit import util PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem" PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem" diff --git a/tests/unittests/test_handler/test_handler_mcollective.py b/tests/unittests/test_handler/test_handler_mcollective.py index 0e3fcc8c..f9448d80 100644 --- a/tests/unittests/test_handler/test_handler_mcollective.py +++ b/tests/unittests/test_handler/test_handler_mcollective.py @@ -43,18 +43,18 @@ class TestConfig(helpers.FilesystemMockingTestCase): contents = util.load_file("/etc/mcollective/server.cfg", decode=False) contents = configobj.ConfigObj(BytesIO(contents)) expected = { - 'loglevel': 'debug', - 'connector': 'rabbitmq', - 'logfile': '/var/log/mcollective.log', - 'ttl': '4294957', - 'collectives': 'mcollective', - 'main_collective': 'mcollective', - 'securityprovider': 'psk', - 'daemonize': '1', - 'factsource': 'yaml', - 'direct_addressing': '1', - 'plugin.psk': 'unset', - 'libdir': '/usr/share/mcollective/plugins', - 'identity': '1', - } + 'loglevel': 'debug', + 'connector': 'rabbitmq', + 'logfile': '/var/log/mcollective.log', + 'ttl': '4294957', + 'collectives': 'mcollective', + 'main_collective': 'mcollective', + 'securityprovider': 'psk', + 'daemonize': '1', + 'factsource': 'yaml', + 'direct_addressing': '1', + 'plugin.psk': 'unset', + 'libdir': '/usr/share/mcollective/plugins', + 'identity': '1', + } self.assertEqual(expected, dict(contents)) -- cgit v1.2.3 From cd1bd176d2f83c2718a0bf921af7ee9116b204c0 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Jul 2016 11:29:41 -0400 Subject: give Sergii credit in changelog --- ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog b/ChangeLog index fa5e7df4..bae982e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -126,6 +126,7 @@ - support network rendering to sysconfig (for centos and RHEL) - write_files: if no permissions are given, just use default without warn. - user_data: fix error when user-data is not utf-8 decodable (LP: #1532072) + - fix mcollective module with python3 (LP: #1597699) [Sergii Golovatiuk] 0.7.6: - open 0.7.6 -- cgit v1.2.3 From 81fce7afe33eaffe66c1be87ea84f0e5580f164a Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Jul 2016 14:13:32 -0400 Subject: add test of apply_network fallback path. we could do this more simply by mocking fbsd.apply_network and checking it's inputs. but this pushes it through the whole path that the other test does. --- tests/unittests/test_distros/test_netconfig.py | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index 9172e3aa..e3168a4a 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -319,3 +319,64 @@ defaultrouter="192.168.1.254" ''' self.assertCfgEquals(expected_buf, str(write_buf)) self.assertEqual(write_buf.mode, 0o644) + + def test_apply_network_config_fallback(self): + fbsd_distro = self._get_distro('freebsd') + + # a weak attempt to verify that we don't have an implementation + # of _write_network_config or apply_network_config in fbsd now, + # which would make this test not actually test the fallback. + self.assertRaises( + NotImplementedError, fbsd_distro._write_network_config, + BASE_NET_CFG) + + # now run + mynetcfg = { + 'config': [{"type": "physical", "name": "eth0", + "mac_address": "c0:d6:9f:2c:e8:80", + "subnets": [{"type": "dhcp"}]}], + 'version': 1} + + write_bufs = {} + read_bufs = { + '/etc/rc.conf': '', + '/etc/resolv.conf': '', + } + + def replace_write(filename, content, mode=0o644, omode="wb"): + buf = WriteBuffer() + buf.mode = mode + buf.omode = omode + buf.write(content) + write_bufs[filename] = buf + + def replace_read(fname, read_cb=None, quiet=False): + if fname not in read_bufs: + if fname in write_bufs: + return str(write_bufs[fname]) + raise IOError("%s not found" % fname) + else: + if fname in write_bufs: + return str(write_bufs[fname]) + return read_bufs[fname] + + with ExitStack() as mocks: + mocks.enter_context( + mock.patch.object(util, 'subp', return_value=('vtnet0', ''))) + mocks.enter_context( + mock.patch.object(os.path, 'exists', return_value=False)) + mocks.enter_context( + mock.patch.object(util, 'write_file', replace_write)) + mocks.enter_context( + mock.patch.object(util, 'load_file', replace_read)) + + fbsd_distro.apply_network_config(mynetcfg, bring_up=False) + + self.assertIn('/etc/rc.conf', write_bufs) + write_buf = write_bufs['/etc/rc.conf'] + expected_buf = ''' +ifconfig_vtnet0="DHCP" +''' + self.assertCfgEquals(expected_buf, str(write_buf)) + self.assertEqual(write_buf.mode, 0o644) + -- cgit v1.2.3 From 9a0189ccda40e366517c0690e0ea2833e533bf1a Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 14 Jul 2016 14:17:52 -0400 Subject: flake8 --- tests/unittests/test_distros/test_netconfig.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index e3168a4a..36eae2dc 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -379,4 +379,3 @@ ifconfig_vtnet0="DHCP" ''' self.assertCfgEquals(expected_buf, str(write_buf)) self.assertEqual(write_buf.mode, 0o644) - -- cgit v1.2.3