From dd03bb411c9a6f10854a3bbc3223b204c3d4d174 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 9 May 2017 20:23:05 -0600 Subject: sysconfig: Raise ValueError when multiple default gateways are present. Fixed setting Route.has_set_default_ipv6 or *_ipv4 to track whether a route already has a default gateway defined. The code was setting Route.has_set_default which wasn't checked when raising "duplicate gateway" ValueErrors. Added unit tests to exercise this expected raised ValueError. Also moved is_ipv6 = subnet.get('ipv6') logic out of a for loop because we don't need to recalculate the same value every route iteration. LP: #1687485 --- cloudinit/net/sysconfig.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 504e4d02..d981277a 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -232,12 +232,8 @@ class Renderer(renderer.Renderer): iface_cfg.name)) if 'netmask' in subnet: iface_cfg['NETMASK'] = subnet['netmask'] + is_ipv6 = subnet.get('ipv6') for route in subnet.get('routes', []): - if subnet.get('ipv6'): - gw_cfg = 'IPV6_DEFAULTGW' - else: - gw_cfg = 'GATEWAY' - if _is_default_route(route): if ( (subnet.get('ipv4') and @@ -258,8 +254,12 @@ class Renderer(renderer.Renderer): # also provided the default route? iface_cfg['DEFROUTE'] = True if 'gateway' in route: - iface_cfg[gw_cfg] = route['gateway'] - route_cfg.has_set_default = True + if is_ipv6: + iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] + route_cfg.has_set_default_ipv6 = True + else: + iface_cfg['GATEWAY'] = route['gateway'] + route_cfg.has_set_default_ipv4 = True else: gw_key = 'GATEWAY%s' % route_cfg.last_idx nm_key = 'NETMASK%s' % route_cfg.last_idx -- cgit v1.2.3 From 9d437489b8ce1f8cd9d34cd9ff4994ca18bd2d78 Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Tue, 16 May 2017 09:19:11 +0000 Subject: Add address to config entry generated by _klibc_to_config_entry. If /run/net-.cfg contains an IPV4ADDR or an IPV6ADDR, the config file generated by _klibc_to_config_entry now contains the "address". LP: #1691135 --- cloudinit/net/cmdline.py | 5 +++++ tests/unittests/test_net.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py index 7c5d11a7..61e23697 100755 --- a/cloudinit/net/cmdline.py +++ b/cloudinit/net/cmdline.py @@ -100,6 +100,11 @@ def _klibc_to_config_entry(content, mac_addrs=None): cur_proto = data.get(pre + 'PROTO', proto) subnet = {'type': cur_proto, 'control': 'manual'} + # only populate address for static types. While the rendered config + # may have an address for dhcp, that is not really expected. + if cur_proto == 'static': + subnet['address'] = data[pre + 'ADDR'] + # these fields go right on the subnet for key in ('NETMASK', 'BROADCAST', 'GATEWAY'): if pre + key in data: diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 7c5dc4ef..052c4016 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -100,7 +100,8 @@ STATIC_EXPECTED_1 = { 'gateway': '10.0.0.1', 'dns_search': ['foo.com'], 'type': 'static', 'netmask': '255.255.255.0', - 'dns_nameservers': ['10.0.1.1']}], + 'dns_nameservers': ['10.0.1.1'], + 'address': '10.0.0.2'}], } # Examples (and expected outputs for various renderers). -- cgit v1.2.3 From 4bcc947301bedc5ebf430cfaf6e4597bfb174aa7 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 16 May 2017 13:43:55 -0400 Subject: Improve detection of snappy to include os-release and kernel cmdline. Recent core snap images (edge channel revision 1886) do not contain the previously known files used to detect that a system is ubuntu core. The changes here are to look in 2 additional locations to determine if a system is snappy. LP: #1689944 --- cloudinit/net/cmdline.py | 31 +------------------- cloudinit/util.py | 37 ++++++++++++++++++++++++ tests/unittests/helpers.py | 7 ++--- tests/unittests/test_util.py | 69 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 34 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py index 61e23697..38b27a52 100755 --- a/cloudinit/net/cmdline.py +++ b/cloudinit/net/cmdline.py @@ -9,41 +9,12 @@ import base64 import glob import gzip import io -import shlex -import sys - -import six from . import get_devicelist from . import read_sys_net_safe from cloudinit import util -PY26 = sys.version_info[0:2] == (2, 6) - - -def _shlex_split(blob): - if PY26 and isinstance(blob, six.text_type): - # Older versions don't support unicode input - blob = blob.encode("utf8") - return shlex.split(blob) - - -def _load_shell_content(content, add_empty=False, empty_val=None): - """Given shell like syntax (key=value\nkey2=value2\n) in content - return the data in dictionary form. If 'add_empty' is True - then add entries in to the returned dictionary for 'VAR=' - variables. Set their value to empty_val.""" - data = {} - for line in _shlex_split(content): - key, value = line.split("=", 1) - if not value: - value = empty_val - if add_empty or value: - data[key] = value - - return data - def _klibc_to_config_entry(content, mac_addrs=None): """Convert a klibc written shell content file to a 'config' entry @@ -63,7 +34,7 @@ def _klibc_to_config_entry(content, mac_addrs=None): if mac_addrs is None: mac_addrs = {} - data = _load_shell_content(content) + data = util.load_shell_content(content) try: name = data['DEVICE'] if 'DEVICE' in data else data['DEVICE6'] except KeyError: diff --git a/cloudinit/util.py b/cloudinit/util.py index 27a98330..67ff7ba3 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -24,6 +24,7 @@ import platform import pwd import random import re +import shlex import shutil import socket import stat @@ -75,6 +76,7 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'], PROC_CMDLINE = None _LSB_RELEASE = {} +PY26 = sys.version_info[0:2] == (2, 6) def get_architecture(target=None): @@ -2424,6 +2426,18 @@ def system_is_snappy(): # channel.ini is configparser loadable. # snappy will move to using /etc/system-image/config.d/*.ini # this is certainly not a perfect test, but good enough for now. + orpath = "/etc/os-release" + try: + orinfo = load_shell_content(load_file(orpath, quiet=True)) + if orinfo.get('ID', '').lower() == "ubuntu-core": + return True + except ValueError as e: + LOG.warning("Unexpected error loading '%s': %s", orpath, e) + + cmdline = get_cmdline() + if 'snap_core=' in cmdline: + return True + content = load_file("/etc/system-image/channel.ini", quiet=True) if 'ubuntu-core' in content.lower(): return True @@ -2470,4 +2484,27 @@ def rootdev_from_cmdline(cmdline): return "/dev/" + found +def load_shell_content(content, add_empty=False, empty_val=None): + """Given shell like syntax (key=value\nkey2=value2\n) in content + return the data in dictionary form. If 'add_empty' is True + then add entries in to the returned dictionary for 'VAR=' + variables. Set their value to empty_val.""" + + def _shlex_split(blob): + if PY26 and isinstance(blob, six.text_type): + # Older versions don't support unicode input + blob = blob.encode("utf8") + return shlex.split(blob) + + data = {} + for line in _shlex_split(content): + key, value = line.split("=", 1) + if not value: + value = empty_val + if add_empty or value: + data[key] = value + + return data + + # vi: ts=4 expandtab diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index a711404c..d24f817d 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -106,7 +106,7 @@ class CiTestCase(TestCase): return os.path.normpath(os.path.abspath(os.path.join(dir, path))) -class ResourceUsingTestCase(TestCase): +class ResourceUsingTestCase(CiTestCase): def setUp(self): super(ResourceUsingTestCase, self).setUp() self.resource_path = None @@ -229,8 +229,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): def reRoot(self, root=None): if root is None: - root = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, root) + root = self.tmp_dir() self.patchUtils(root) self.patchOS(root) return root @@ -256,7 +255,7 @@ def populate_dir(path, files): os.makedirs(path) ret = [] for (name, content) in files.items(): - p = os.path.join(path, name) + p = os.path.sep.join([path, name]) util.ensure_dir(os.path.dirname(p)) with open(p, "wb") as fp: if isinstance(content, six.binary_type): diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 189caca8..490760d1 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -712,4 +712,73 @@ class TestProcessExecutionError(helpers.TestCase): )).format(description=self.empty_description, empty_attr=self.empty_attr)) + +class TestSystemIsSnappy(helpers.FilesystemMockingTestCase): + def test_id_in_os_release_quoted(self): + """os-release containing ID="ubuntu-core" is snappy.""" + orcontent = '\n'.join(['ID="ubuntu-core"', '']) + root_d = self.tmp_dir() + helpers.populate_dir(root_d, {'etc/os-release': orcontent}) + self.reRoot(root_d) + self.assertTrue(util.system_is_snappy()) + + def test_id_in_os_release(self): + """os-release containing ID=ubuntu-core is snappy.""" + orcontent = '\n'.join(['ID=ubuntu-core', '']) + root_d = self.tmp_dir() + helpers.populate_dir(root_d, {'etc/os-release': orcontent}) + self.reRoot(root_d) + self.assertTrue(util.system_is_snappy()) + + @mock.patch('cloudinit.util.get_cmdline') + def test_bad_content_in_os_release_no_effect(self, m_cmdline): + """malformed os-release should not raise exception.""" + m_cmdline.return_value = 'root=/dev/sda' + orcontent = '\n'.join(['IDubuntu-core', '']) + root_d = self.tmp_dir() + helpers.populate_dir(root_d, {'etc/os-release': orcontent}) + self.reRoot() + self.assertFalse(util.system_is_snappy()) + + @mock.patch('cloudinit.util.get_cmdline') + def test_snap_core_in_cmdline_is_snappy(self, m_cmdline): + """The string snap_core= in kernel cmdline indicates snappy.""" + cmdline = ( + "BOOT_IMAGE=(loop)/kernel.img root=LABEL=writable " + "snap_core=core_x1.snap snap_kernel=pc-kernel_x1.snap ro " + "net.ifnames=0 init=/lib/systemd/systemd console=tty1 " + "console=ttyS0 panic=-1") + m_cmdline.return_value = cmdline + self.assertTrue(util.system_is_snappy()) + self.assertTrue(m_cmdline.call_count > 0) + + @mock.patch('cloudinit.util.get_cmdline') + def test_nothing_found_is_not_snappy(self, m_cmdline): + """If no positive identification, then not snappy.""" + m_cmdline.return_value = 'root=/dev/sda' + self.reRoot() + self.assertFalse(util.system_is_snappy()) + self.assertTrue(m_cmdline.call_count > 0) + + @mock.patch('cloudinit.util.get_cmdline') + def test_channel_ini_with_snappy_is_snappy(self, m_cmdline): + """A Channel.ini file with 'ubuntu-core' indicates snappy.""" + m_cmdline.return_value = 'root=/dev/sda' + root_d = self.tmp_dir() + content = '\n'.join(["[Foo]", "source = 'ubuntu-core'", ""]) + helpers.populate_dir( + root_d, {'etc/system-image/channel.ini': content}) + self.reRoot(root_d) + self.assertTrue(util.system_is_snappy()) + + @mock.patch('cloudinit.util.get_cmdline') + def test_system_image_config_dir_is_snappy(self, m_cmdline): + """Existence of /etc/system-image/config.d indicates snappy.""" + m_cmdline.return_value = 'root=/dev/sda' + root_d = self.tmp_dir() + helpers.populate_dir( + root_d, {'etc/system-image/config.d/my.file': "_unused"}) + self.reRoot(root_d) + self.assertTrue(util.system_is_snappy()) + # vi: ts=4 expandtab -- cgit v1.2.3 From a6572d9415e592cbf9821b769bbee9e7fdf029d5 Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Sat, 13 May 2017 01:28:14 +0100 Subject: netplan: fix netplan render_network_state signature. tools/net-convert fails to output netplan config, because the positional arguments of render_network_state are the wrong way around for that function w.r.t. other renders. Fix the netplan renderer to have the correct signature. LP: #1685944 --- cloudinit/net/netplan.py | 2 +- tests/unittests/test_net.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 825fe831..56b41be4 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -205,7 +205,7 @@ class Renderer(renderer.Renderer): self._postcmds = config.get('postcmds', False) self.clean_default = config.get('clean_default', True) - def render_network_state(self, target, network_state): + def render_network_state(self, network_state, target): # check network state for version # if v2, then extract network_state.config # else render_v2_from_state diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 052c4016..d36d0e76 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -1073,7 +1073,7 @@ class TestNetplanNetRendering(CiTestCase): render_target = 'netplan.yaml' renderer = netplan.Renderer( {'netplan_path': render_target, 'postcmds': False}) - renderer.render_network_state(render_dir, ns) + renderer.render_network_state(ns, render_dir) self.assertTrue(os.path.exists(os.path.join(render_dir, render_target))) @@ -1178,7 +1178,7 @@ class TestNetplanPostcommands(CiTestCase): render_target = 'netplan.yaml' renderer = netplan.Renderer( {'netplan_path': render_target, 'postcmds': True}) - renderer.render_network_state(render_dir, ns) + renderer.render_network_state(ns, render_dir) mock_netplan_generate.assert_called_with(run=True) mock_net_setup_link.assert_called_with(run=True) @@ -1203,7 +1203,7 @@ class TestNetplanPostcommands(CiTestCase): '/sys/class/net/lo'], capture=True), ] with mock.patch.object(os.path, 'islink', return_value=True): - renderer.render_network_state(render_dir, ns) + renderer.render_network_state(ns, render_dir) mock_subp.assert_has_calls(expected) @@ -1354,9 +1354,9 @@ class TestCmdlineReadKernelConfig(CiTestCase): class TestNetplanRoundTrip(CiTestCase): def _render_and_read(self, network_config=None, state=None, - netplan_path=None, dir=None): - if dir is None: - dir = self.tmp_dir() + netplan_path=None, target=None): + if target is None: + target = self.tmp_dir() if network_config: ns = network_state.parse_net_config_data(network_config) @@ -1371,8 +1371,8 @@ class TestNetplanRoundTrip(CiTestCase): renderer = netplan.Renderer( config={'netplan_path': netplan_path}) - renderer.render_network_state(dir, ns) - return dir2dict(dir) + renderer.render_network_state(ns, target) + return dir2dict(target) def testsimple_render_small_netplan(self): entry = NETWORK_CONFIGS['small'] -- cgit v1.2.3 From d059d480c3a5bbeb3bb2e8ff2350f85d64721c11 Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Sat, 13 May 2017 16:15:37 +0100 Subject: netplan: pass macaddress, when specified, for vlans When vlan mac address is specified in config, render it for netplan and for ENI. LP: #1690388 --- cloudinit/net/netplan.py | 4 +++- tests/unittests/test_net.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 56b41be4..9b71de97 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -345,7 +345,9 @@ class Renderer(renderer.Renderer): 'id': ifcfg.get('vlan_id'), 'link': ifcfg.get('vlan-raw-device') } - + macaddr = ifcfg.get('mac_address', None) + if macaddr is not None: + vlan['macaddress'] = macaddr.lower() _extract_addresses(ifcfg, vlan) vlans.update({ifname: vlan}) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index d36d0e76..68a0157a 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -558,6 +558,7 @@ iface eth0.101 inet static dns-nameservers 192.168.0.10 10.23.23.134 dns-search barley.maas sacchromyces.maas brettanomyces.maas gateway 192.168.0.1 + hwaddress aa:bb:cc:dd:ee:11 mtu 1500 vlan-raw-device eth0 vlan_id 101 @@ -680,6 +681,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true gateway4: 192.168.0.1 id: 101 link: eth0 + macaddress: aa:bb:cc:dd:ee:11 nameservers: addresses: - 192.168.0.10 @@ -723,6 +725,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true name: eth0.101 vlan_link: eth0 vlan_id: 101 + mac_address: aa:bb:cc:dd:ee:11 mtu: 1500 subnets: - type: static -- cgit v1.2.3 From 2c0655feb9a194b5fbdfe90a5f847c16f1e15409 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Fri, 19 May 2017 13:12:54 -0400 Subject: Fix get_interfaces_by_mac for empty macs Some interfaces (greptap0 in the bug) have a mac address of '00:00:00:00:00:00'. That was causing a duplicate mac detection as the 'lo' device also has that mac. The change here is to just ignore macs other than 'lo' that have that. LP: #1692028 --- cloudinit/net/__init__.py | 3 +++ tests/unittests/test_net.py | 27 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index a072a8d6..8c6cd057 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -393,6 +393,7 @@ def get_interfaces_by_mac(): else: raise ret = {} + empty_mac = '00:00:00:00:00:00' for name in devs: if not interface_has_own_mac(name): continue @@ -404,6 +405,8 @@ def get_interfaces_by_mac(): # some devices may not have a mac (tun0) if not mac: continue + if mac == empty_mac and name != 'lo': + continue if mac in ret: raise RuntimeError( "duplicate mac found! both '%s' and '%s' have mac '%s'" % diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 68a0157a..8bd3f433 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -1542,24 +1542,24 @@ class TestNetRenderers(CiTestCase): class TestGetInterfacesByMac(CiTestCase): - _data = {'devices': ['enp0s1', 'enp0s2', 'bond1', 'bridge1', - 'bridge1-nic', 'tun0', 'bond1.101'], - 'bonds': ['bond1'], + _data = {'bonds': ['bond1'], 'bridges': ['bridge1'], 'vlans': ['bond1.101'], 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1', - 'bond1.101'], + 'bond1.101', 'lo'], 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01', 'enp0s2': 'aa:aa:aa:aa:aa:02', 'bond1': 'aa:aa:aa:aa:aa:01', 'bond1.101': 'aa:aa:aa:aa:aa:01', 'bridge1': 'aa:aa:aa:aa:aa:03', 'bridge1-nic': 'aa:aa:aa:aa:aa:03', + 'lo': '00:00:00:00:00:00', + 'greptap0': '00:00:00:00:00:00', 'tun0': None}} data = {} def _se_get_devicelist(self): - return self.data['devices'] + return list(self.data['devices']) def _se_get_interface_mac(self, name): return self.data['macs'][name] @@ -1575,6 +1575,7 @@ class TestGetInterfacesByMac(CiTestCase): def _mock_setup(self): self.data = copy.deepcopy(self._data) + self.data['devices'] = set(list(self.data['macs'].keys())) mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge', 'interface_has_own_mac', 'is_vlan') self.mocks = {} @@ -1602,7 +1603,7 @@ class TestGetInterfacesByMac(CiTestCase): [mock.call('enp0s1'), mock.call('bond1')], any_order=True) self.assertEqual( {'aa:aa:aa:aa:aa:01': 'enp0s1', 'aa:aa:aa:aa:aa:02': 'enp0s2', - 'aa:aa:aa:aa:aa:03': 'bridge1-nic'}, + 'aa:aa:aa:aa:aa:03': 'bridge1-nic', '00:00:00:00:00:00': 'lo'}, ret) def test_excludes_bridges(self): @@ -1611,7 +1612,7 @@ class TestGetInterfacesByMac(CiTestCase): # set everything other than 'b1' to be a bridge. # then expect b1 is the only thing left. self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1' - self.data['devices'].append('b1') + self.data['devices'].add('b1') self.data['bonds'] = [] self.data['own_macs'] = self.data['devices'] self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"] @@ -1628,7 +1629,7 @@ class TestGetInterfacesByMac(CiTestCase): # set everything other than 'b1' to be a vlan. # then expect b1 is the only thing left. self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1' - self.data['devices'].append('b1') + self.data['devices'].add('b1') self.data['bonds'] = [] self.data['bridges'] = [] self.data['own_macs'] = self.data['devices'] @@ -1640,6 +1641,16 @@ class TestGetInterfacesByMac(CiTestCase): mock.call('b1')], any_order=True) + def test_duplicates_of_empty_mac_are_ok(self): + """Duplicate macs of 00:00:00:00:00:00 should be skipped.""" + self._mock_setup() + empty_mac = "00:00:00:00:00:00" + addnics = ('greptap1', 'lo', 'greptap2') + self.data['macs'].update(dict((k, empty_mac) for k in addnics)) + self.data['devices'].update(set(addnics)) + ret = net.get_interfaces_by_mac() + self.assertEqual('lo', ret[empty_mac]) + def _gzip_data(data): with io.BytesIO() as iobuf: -- cgit v1.2.3 From f38fa41317602908139aa96e930b634f65e39555 Mon Sep 17 00:00:00 2001 From: Andreas Karis Date: Fri, 21 Apr 2017 20:35:39 -0400 Subject: RHEL/CentOS: Fix dual stack IPv4/IPv6 configuration. Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7. This patch fixes several scenarios for IPv4/IPv6/dual-stack with multiple IP assignment. Removes usage of unpopular IPv4 alias files and invalid IPv6 alias files. Also fix associated unit tests. LP: #1679817 LP: #1685534 LP: #1685532 --- cloudinit/net/sysconfig.py | 244 ++++++++++++++++++------- tests/unittests/test_distros/test_netconfig.py | 8 +- tests/unittests/test_net.py | 79 +++----- 3 files changed, 199 insertions(+), 132 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index d981277a..58c5713f 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -59,6 +59,9 @@ class ConfigMap(object): def __setitem__(self, key, value): self._conf[key] = value + def __getitem__(self, key): + return self._conf[key] + def drop(self, key): self._conf.pop(key, None) @@ -83,7 +86,8 @@ class ConfigMap(object): class Route(ConfigMap): """Represents a route configuration.""" - route_fn_tpl = '%(base)s/network-scripts/route-%(name)s' + route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s' + route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s' def __init__(self, route_name, base_sysconf_dir): super(Route, self).__init__() @@ -102,9 +106,58 @@ class Route(ConfigMap): return r @property - def path(self): - return self.route_fn_tpl % ({'base': self._base_sysconf_dir, - 'name': self._route_name}) + def path_ipv4(self): + return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir, + 'name': self._route_name}) + + @property + def path_ipv6(self): + return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir, + 'name': self._route_name}) + + def is_ipv6_route(self, address): + return ':' in address + + def to_string(self, proto="ipv4"): + # only accept ipv4 and ipv6 + if proto not in ['ipv4', 'ipv6']: + raise ValueError("Unknown protocol '%s'" % (str(proto))) + buf = six.StringIO() + buf.write(_make_header()) + if self._conf: + buf.write("\n") + # need to reindex IPv4 addresses + # (because Route can contain a mix of IPv4 and IPv6) + reindex = -1 + for key in sorted(self._conf.keys()): + if 'ADDRESS' in key: + index = key.replace('ADDRESS', '') + address_value = str(self._conf[key]) + # only accept combinations: + # if proto ipv6 only display ipv6 routes + # if proto ipv4 only display ipv4 routes + # do not add ipv6 routes if proto is ipv4 + # do not add ipv4 routes if proto is ipv6 + # (this array will contain a mix of ipv4 and ipv6) + if proto == "ipv4" and not self.is_ipv6_route(address_value): + netmask_value = str(self._conf['NETMASK' + index]) + gateway_value = str(self._conf['GATEWAY' + index]) + # increase IPv4 index + reindex = reindex + 1 + buf.write("%s=%s\n" % ('ADDRESS' + str(reindex), + _quote_value(address_value))) + buf.write("%s=%s\n" % ('GATEWAY' + str(reindex), + _quote_value(gateway_value))) + buf.write("%s=%s\n" % ('NETMASK' + str(reindex), + _quote_value(netmask_value))) + elif proto == "ipv6" and self.is_ipv6_route(address_value): + netmask_value = str(self._conf['NETMASK' + index]) + gateway_value = str(self._conf['GATEWAY' + index]) + buf.write("%s/%s via %s\n" % (address_value, + netmask_value, + gateway_value)) + + return buf.getvalue() class NetInterface(ConfigMap): @@ -211,65 +264,119 @@ class Renderer(renderer.Renderer): iface_cfg[new_key] = old_value @classmethod - def _render_subnet(cls, iface_cfg, route_cfg, subnet): - subnet_type = subnet.get('type') - if subnet_type == 'dhcp6': - iface_cfg['DHCPV6C'] = True - iface_cfg['IPV6INIT'] = True - iface_cfg['BOOTPROTO'] = 'dhcp' - elif subnet_type in ['dhcp4', 'dhcp']: - iface_cfg['BOOTPROTO'] = 'dhcp' - elif subnet_type == 'static': - iface_cfg['BOOTPROTO'] = 'static' - if subnet_is_ipv6(subnet): - iface_cfg['IPV6ADDR'] = subnet['address'] + def _render_subnets(cls, iface_cfg, subnets): + # setting base values + iface_cfg['BOOTPROTO'] = 'none' + + # modifying base values according to subnets + for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): + subnet_type = subnet.get('type') + if subnet_type == 'dhcp6': iface_cfg['IPV6INIT'] = True + iface_cfg['DHCPV6C'] = True + iface_cfg['BOOTPROTO'] = 'dhcp' + elif subnet_type in ['dhcp4', 'dhcp']: + iface_cfg['BOOTPROTO'] = 'dhcp' + elif subnet_type == 'static': + # grep BOOTPROTO sysconfig.txt -A2 | head -3 + # BOOTPROTO=none|bootp|dhcp + # 'bootp' or 'dhcp' cause a DHCP client + # to run on the device. Any other + # value causes any static configuration + # in the file to be applied. + # ==> the following should not be set to 'static' + # but should remain 'none' + # if iface_cfg['BOOTPROTO'] == 'none': + # iface_cfg['BOOTPROTO'] = 'static' + if subnet_is_ipv6(subnet): + iface_cfg['IPV6INIT'] = True else: - iface_cfg['IPADDR'] = subnet['address'] - else: - raise ValueError("Unknown subnet type '%s' found" - " for interface '%s'" % (subnet_type, - iface_cfg.name)) - if 'netmask' in subnet: - iface_cfg['NETMASK'] = subnet['netmask'] - is_ipv6 = subnet.get('ipv6') - for route in subnet.get('routes', []): - if _is_default_route(route): - if ( - (subnet.get('ipv4') and - route_cfg.has_set_default_ipv4) or - (subnet.get('ipv6') and - route_cfg.has_set_default_ipv6) - ): - raise ValueError("Duplicate declaration of default " - "route found for interface '%s'" - % (iface_cfg.name)) - # NOTE(harlowja): ipv6 and ipv4 default gateways - gw_key = 'GATEWAY0' - nm_key = 'NETMASK0' - addr_key = 'ADDRESS0' - # The owning interface provides the default route. - # - # TODO(harlowja): add validation that no other iface has - # also provided the default route? - iface_cfg['DEFROUTE'] = True - if 'gateway' in route: - if is_ipv6: - iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] - route_cfg.has_set_default_ipv6 = True + raise ValueError("Unknown subnet type '%s' found" + " for interface '%s'" % (subnet_type, + iface_cfg.name)) + + # set IPv4 and IPv6 static addresses + ipv4_index = -1 + ipv6_index = -1 + for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): + subnet_type = subnet.get('type') + if subnet_type == 'dhcp6': + continue + elif subnet_type in ['dhcp4', 'dhcp']: + continue + elif subnet_type == 'static': + if subnet_is_ipv6(subnet): + ipv6_index = ipv6_index + 1 + if 'netmask' in subnet and str(subnet['netmask']) != "": + ipv6_cidr = (subnet['address'] + + '/' + + str(subnet['netmask'])) else: - iface_cfg['GATEWAY'] = route['gateway'] - route_cfg.has_set_default_ipv4 = True - else: - gw_key = 'GATEWAY%s' % route_cfg.last_idx - nm_key = 'NETMASK%s' % route_cfg.last_idx - addr_key = 'ADDRESS%s' % route_cfg.last_idx - route_cfg.last_idx += 1 - for (old_key, new_key) in [('gateway', gw_key), - ('netmask', nm_key), - ('network', addr_key)]: - if old_key in route: - route_cfg[new_key] = route[old_key] + ipv6_cidr = subnet['address'] + if ipv6_index == 0: + iface_cfg['IPV6ADDR'] = ipv6_cidr + elif ipv6_index == 1: + iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr + else: + iface_cfg['IPV6ADDR_SECONDARIES'] = ( + iface_cfg['IPV6ADDR_SECONDARIES'] + + " " + ipv6_cidr) + else: + ipv4_index = ipv4_index + 1 + if ipv4_index == 0: + iface_cfg['IPADDR'] = subnet['address'] + if 'netmask' in subnet: + iface_cfg['NETMASK'] = subnet['netmask'] + else: + iface_cfg['IPADDR' + str(ipv4_index)] = \ + subnet['address'] + if 'netmask' in subnet: + iface_cfg['NETMASK' + str(ipv4_index)] = \ + subnet['netmask'] + + @classmethod + def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets): + for i, subnet in enumerate(subnets, start=len(iface_cfg.children)): + for route in subnet.get('routes', []): + is_ipv6 = subnet.get('ipv6') + + if _is_default_route(route): + if ( + (subnet.get('ipv4') and + route_cfg.has_set_default_ipv4) or + (subnet.get('ipv6') and + route_cfg.has_set_default_ipv6) + ): + raise ValueError("Duplicate declaration of default " + "route found for interface '%s'" + % (iface_cfg.name)) + # NOTE(harlowja): ipv6 and ipv4 default gateways + gw_key = 'GATEWAY0' + nm_key = 'NETMASK0' + addr_key = 'ADDRESS0' + # The owning interface provides the default route. + # + # TODO(harlowja): add validation that no other iface has + # also provided the default route? + iface_cfg['DEFROUTE'] = True + if 'gateway' in route: + if is_ipv6: + iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] + route_cfg.has_set_default_ipv6 = True + else: + iface_cfg['GATEWAY'] = route['gateway'] + route_cfg.has_set_default_ipv4 = True + + else: + gw_key = 'GATEWAY%s' % route_cfg.last_idx + nm_key = 'NETMASK%s' % route_cfg.last_idx + addr_key = 'ADDRESS%s' % route_cfg.last_idx + route_cfg.last_idx += 1 + for (old_key, new_key) in [('gateway', gw_key), + ('netmask', nm_key), + ('network', addr_key)]: + if old_key in route: + route_cfg[new_key] = route[old_key] @classmethod def _render_bonding_opts(cls, iface_cfg, iface): @@ -295,15 +402,9 @@ class Renderer(renderer.Renderer): iface_subnets = iface.get("subnets", []) iface_cfg = iface_contents[iface_name] route_cfg = iface_cfg.routes - if len(iface_subnets) == 1: - cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0]) - elif len(iface_subnets) > 1: - for i, isubnet in enumerate(iface_subnets, - start=len(iface_cfg.children)): - iface_sub_cfg = iface_cfg.copy() - iface_sub_cfg.name = "%s:%s" % (iface_name, i) - iface_cfg.children.append(iface_sub_cfg) - cls._render_subnet(iface_sub_cfg, route_cfg, isubnet) + + cls._render_subnets(iface_cfg, iface_subnets) + cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) @classmethod def _render_bond_interfaces(cls, network_state, iface_contents): @@ -387,7 +488,10 @@ class Renderer(renderer.Renderer): if iface_cfg: contents[iface_cfg.path] = iface_cfg.to_string() if iface_cfg.routes: - contents[iface_cfg.routes.path] = iface_cfg.routes.to_string() + contents[iface_cfg.routes.path_ipv4] = \ + iface_cfg.routes.to_string("ipv4") + contents[iface_cfg.routes.path_ipv6] = \ + iface_cfg.routes.to_string("ipv6") return contents def render_network_state(self, network_state, target=None): diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index 1e10a33d..fd7c051f 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -476,7 +476,7 @@ NETWORKING=yes expected_buf = ''' # Created by cloud-init on instance boot automatically, do not edit. # -BOOTPROTO=static +BOOTPROTO=none DEVICE=eth0 IPADDR=192.168.1.5 NETMASK=255.255.255.0 @@ -533,7 +533,6 @@ NETWORKING=yes mock.patch.object(util, 'load_file', return_value='')) mocks.enter_context( mock.patch.object(os.path, 'isfile', return_value=False)) - rh_distro.apply_network(BASE_NET_CFG_IPV6, False) self.assertEqual(len(write_bufs), 4) @@ -626,11 +625,10 @@ IPV6_AUTOCONF=no expected_buf = ''' # Created by cloud-init on instance boot automatically, do not edit. # -BOOTPROTO=static +BOOTPROTO=none DEVICE=eth0 -IPV6ADDR=2607:f0d0:1002:0011::2 +IPV6ADDR=2607:f0d0:1002:0011::2/64 IPV6INIT=yes -NETMASK=64 NM_CONTROLLED=no ONBOOT=yes TYPE=Ethernet diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 8bd3f433..feeab908 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -137,7 +137,7 @@ OS_SAMPLES = [ """ # Created by cloud-init on instance boot automatically, do not edit. # -BOOTPROTO=static +BOOTPROTO=none DEFROUTE=yes DEVICE=eth0 GATEWAY=172.19.3.254 @@ -205,38 +205,14 @@ nameserver 172.19.0.12 # Created by cloud-init on instance boot automatically, do not edit. # BOOTPROTO=none -DEVICE=eth0 -HWADDR=fa:16:3e:ed:9a:59 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -""".lstrip()), - ('etc/sysconfig/network-scripts/ifcfg-eth0:0', - """ -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=static DEFROUTE=yes -DEVICE=eth0:0 +DEVICE=eth0 GATEWAY=172.19.3.254 HWADDR=fa:16:3e:ed:9a:59 IPADDR=172.19.1.34 +IPADDR1=10.0.0.10 NETMASK=255.255.252.0 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -""".lstrip()), - ('etc/sysconfig/network-scripts/ifcfg-eth0:1', - """ -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=static -DEVICE=eth0:1 -HWADDR=fa:16:3e:ed:9a:59 -IPADDR=10.0.0.10 -NETMASK=255.255.255.0 +NETMASK1=255.255.255.0 NM_CONTROLLED=no ONBOOT=yes TYPE=Ethernet @@ -266,7 +242,7 @@ nameserver 172.19.0.12 }], "ip_address": "172.19.1.34", "id": "network0" }, { - "network_id": "public-ipv6", + "network_id": "public-ipv6-a", "type": "ipv6", "netmask": "", "link": "tap1a81968a-79", "routes": [ @@ -277,6 +253,20 @@ nameserver 172.19.0.12 } ], "ip_address": "2001:DB8::10", "id": "network1" + }, { + "network_id": "public-ipv6-b", + "type": "ipv6", "netmask": "64", + "link": "tap1a81968a-79", + "routes": [ + ], + "ip_address": "2001:DB9::10", "id": "network2" + }, { + "network_id": "public-ipv6-c", + "type": "ipv6", "netmask": "64", + "link": "tap1a81968a-79", + "routes": [ + ], + "ip_address": "2001:DB10::10", "id": "network3" }], "links": [ { @@ -296,41 +286,16 @@ nameserver 172.19.0.12 # Created by cloud-init on instance boot automatically, do not edit. # BOOTPROTO=none -DEVICE=eth0 -HWADDR=fa:16:3e:ed:9a:59 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -""".lstrip()), - ('etc/sysconfig/network-scripts/ifcfg-eth0:0', - """ -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=static DEFROUTE=yes -DEVICE=eth0:0 +DEVICE=eth0 GATEWAY=172.19.3.254 HWADDR=fa:16:3e:ed:9a:59 IPADDR=172.19.1.34 -NETMASK=255.255.252.0 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -""".lstrip()), - ('etc/sysconfig/network-scripts/ifcfg-eth0:1', - """ -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=static -DEFROUTE=yes -DEVICE=eth0:1 -HWADDR=fa:16:3e:ed:9a:59 IPV6ADDR=2001:DB8::10 +IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64" IPV6INIT=yes IPV6_DEFAULTGW=2001:DB8::1 -NETMASK= +NETMASK=255.255.252.0 NM_CONTROLLED=no ONBOOT=yes TYPE=Ethernet -- cgit v1.2.3 From 910ed46124e992eb20e49ea156b7127cd3ebbe9d Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Sat, 13 May 2017 01:45:23 +0100 Subject: nplan: For bonds, allow dashed or underscore names of keys. As some of the bond paramemters are passed in as dashed, or underscored, depending on the input source. Also correct transmit-hash-policy netplan target key. LP: #1690480 --- cloudinit/net/netplan.py | 4 ++-- tests/unittests/test_net.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 9b71de97..d7ddf0c3 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -41,7 +41,7 @@ NET_CONFIG_TO_V2 = { 'bond-num-grat-arp': 'gratuitious-arp', 'bond-primary-reselect': 'primary-reselect-policy', 'bond-updelay': 'up-delay', - 'bond-xmit_hash_policy': 'transmit_hash_policy'}, + 'bond-xmit-hash-policy': 'transmit-hash-policy'}, 'bridge': {'bridge_ageing': 'ageing-time', 'bridge_bridgeprio': 'priority', 'bridge_fd': 'forward-delay', @@ -294,7 +294,7 @@ class Renderer(renderer.Renderer): for match in ['bond_', 'bond-']: bond_params = _get_params_dict_by_match(ifcfg, match) for (param, value) in bond_params.items(): - newname = v2_bond_map.get(param) + newname = v2_bond_map.get(param.replace('_', '-')) if newname is None: continue bond_config.update({newname: value}) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 7104d00e..5169821a 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -483,11 +483,15 @@ auto eth1 iface eth1 inet manual bond-master bond0 bond-mode active-backup + bond-xmit-hash-policy layer3+4 + bond_miimon 100 auto eth2 iface eth2 inet manual bond-master bond0 bond-mode active-backup + bond-xmit-hash-policy layer3+4 + bond_miimon 100 iface eth3 inet manual @@ -500,6 +504,8 @@ auto bond0 iface bond0 inet6 dhcp bond-mode active-backup bond-slaves none + bond-xmit-hash-policy layer3+4 + bond_miimon 100 hwaddress aa:bb:cc:dd:ee:ff auto br0 @@ -625,7 +631,9 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true - eth1 - eth2 parameters: + mii-monitor-interval: 100 mode: active-backup + transmit-hash-policy: layer3+4 bridges: br0: addresses: @@ -716,6 +724,8 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true - eth2 params: bond-mode: active-backup + bond_miimon: 100 + bond-xmit-hash-policy: "layer3+4" subnets: - type: dhcp6 # A Bond VLAN. -- cgit v1.2.3 From 16a7302f6acb69adb0aee75eaf12392fa3688853 Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Tue, 16 May 2017 14:18:25 +0100 Subject: net: fix reading and rendering addresses in cidr format. Input (specifically OpenStack) that had: "ip_address" : "104.130.20.155", "netmask" : "255.255.255.0" Was being rendered to netplan as '104.130.20.155/255.255.255.0'. That is now fixed to '104.130.20.155/24' Also fixed is reading of a route that had a network prefix integer in the 'netmask' rather than a netmask. LP: #1689346 LP: #1684349 --- cloudinit/net/netplan.py | 14 ++++++++------ cloudinit/net/network_state.py | 4 ++-- tests/unittests/test_distros/test_netconfig.py | 2 +- tests/unittests/test_net.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) (limited to 'cloudinit/net') diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index d7ddf0c3..a715f3b0 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -4,7 +4,7 @@ import copy import os from . import renderer -from .network_state import subnet_is_ipv6 +from .network_state import mask2cidr, subnet_is_ipv6 from cloudinit import log as logging from cloudinit import util @@ -118,9 +118,10 @@ def _extract_addresses(config, entry): sn_type += '4' entry.update({sn_type: True}) elif sn_type in ['static']: - addr = "%s" % subnet.get('address') - if 'netmask' in subnet: - addr += "/%s" % subnet.get('netmask') + addr = '%s' % subnet.get('address') + netmask = subnet.get('netmask') + if netmask and '/' not in addr: + addr += '/%s' % mask2cidr(netmask) if 'gateway' in subnet and subnet.get('gateway'): gateway = subnet.get('gateway') if ":" in gateway: @@ -137,8 +138,9 @@ def _extract_addresses(config, entry): mtukey += '6' entry.update({mtukey: subnet.get('mtu')}) for route in subnet.get('routes', []): - to_net = "%s/%s" % (route.get('network'), - route.get('netmask')) + network = route.get('network') + netmask = route.get('netmask') + to_net = '%s/%s' % (network, mask2cidr(netmask)) route = { 'via': route.get('gateway'), 'to': to_net, diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index db3c3579..9e9c05a0 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -734,9 +734,9 @@ def ipv6mask2cidr(mask): def mask2cidr(mask): - if ':' in mask: + if ':' in str(mask): return ipv6mask2cidr(mask) - elif '.' in mask: + elif '.' in str(mask): return ipv4mask2cidr(mask) else: return mask diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index fd7c051f..be9a8318 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -127,7 +127,7 @@ network: ethernets: eth0: addresses: - - 192.168.1.5/255.255.255.0 + - 192.168.1.5/24 gateway4: 192.168.1.254 eth1: dhcp4: true diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 5169821a..167ed01e 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -406,7 +406,7 @@ NETWORK_CONFIGS = { - sach.maas - wark.maas routes: - - to: 0.0.0.0/0.0.0.0 + - to: 0.0.0.0/0 via: 65.61.151.37 set-name: eth99 """).rstrip(' '), -- cgit v1.2.3