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. --- tests/unittests/test_net.py | 182 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) (limited to 'tests') 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 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(-) (limited to 'tests') 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(-) (limited to 'tests') 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 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(-) (limited to 'tests') 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(-) (limited to 'tests') 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(-) (limited to 'tests') 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 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(-) (limited to 'tests') 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 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 (limited to 'tests') 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(-) (limited to 'tests') 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 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(+) (limited to 'tests') 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(-) (limited to 'tests') 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