From 47c18d2f7b80a3331cf199b45355cdebf4982ea2 Mon Sep 17 00:00:00 2001 From: Ryan Harper Date: Thu, 6 Jul 2017 16:50:41 -0500 Subject: [PATCH] sysconfig: use MACADDR on bonds/bridges to configure mac_address Previously, sysconfig rendered HWADDR for all interface types, but that value is only used to identify physical devices. Instead use MACADDR to configure the MAC on virtual devices, like bonds and bridges. - Sort bond slave list to ensure consistent ordering in sysconfig rendered files. - Add unittests for sysconfig rendering of bonds/bridges with mac_address Fixes LP:#1701417 --- cloudinit/net/sysconfig.py | 18 +++- tests/unittests/test_net.py | 202 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 4 deletions(-) diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 8427cee..95324ea 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -263,6 +263,9 @@ class Renderer(renderer.Renderer): for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]: old_value = iface.get(old_key) if old_value is not None: + # only set HWADDR on physical interfaces + if old_key == 'mac_address' and iface['type'] != 'physical': + continue iface_cfg[new_key] = old_value @classmethod @@ -435,15 +438,18 @@ class Renderer(renderer.Renderer): master_cfg['BONDING_MASTER'] = True master_cfg.kind = 'bond' + if 'mac_address' in iface and iface.get('mac_address'): + iface_cfg['MACADDR'] = iface.get('mac_address') + iface_subnets = iface.get("subnets", []) route_cfg = iface_cfg.routes cls._render_subnets(iface_cfg, iface_subnets) cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets) - bond_slaves = [slave_iface['name'] - for slave_iface in - network_state.iter_interfaces(slave_filter) - if slave_iface['bond-master'] == iface_name] + bond_slaves = sorted([slave_iface['name'] + for slave_iface in + network_state.iter_interfaces(slave_filter) + if slave_iface['bond-master'] == iface_name]) for index, bond_slave in enumerate(bond_slaves): slavestr = 'BONDING_SLAVE%s' % index iface_cfg[slavestr] = bond_slave @@ -502,6 +508,10 @@ class Renderer(renderer.Renderer): for old_key, new_key in cls.bridge_opts_keys: if old_key in iface: iface_cfg[new_key] = iface[old_key] + + if 'mac_address' in iface and iface.get('mac_address'): + iface_cfg['MACADDR'] = iface.get('mac_address') + # Is this the right key to get all the connected interfaces? for bridged_iface_name in iface.get('bridge_ports', []): # Ensure all bridged interfaces are correctly tagged diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 5f75e00..1c4f4de 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -422,6 +422,31 @@ NETWORK_CONFIGS = { via: 65.61.151.37 set-name: eth99 """).rstrip(' '), + 'expected_sysconfig': { + 'ifcfg-eth1': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + DEVICE=eth1 + HWADDR=cf:d6:af:48:e8:80 + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no""").rstrip(' '), + 'ifcfg-eth99': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=dhcp + DEFROUTE=yes + DEVICE=eth99 + GATEWAY=65.61.151.37 + HWADDR=c0:d6:9f:2c:e8:80 + IPADDR=192.168.21.3 + NETMASK=255.255.255.0 + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no """).rstrip(' ')}, 'yaml': textwrap.dedent(""" version: 1 config: @@ -702,6 +727,146 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true - sacchromyces.maas - brettanomyces.maas """).rstrip(' '), + 'expected_sysconfig': { + 'ifcfg-bond0': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BONDING_MASTER=yes + BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100" + BONDING_SLAVE0=eth1 + BONDING_SLAVE1=eth2 + BOOTPROTO=dhcp + DEVICE=bond0 + DHCPV6C=yes + IPV6INIT=yes + MACADDR=aa:bb:cc:dd:ee:ff + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Bond + USERCTL=no + """).rstrip(' '), + 'ifcfg-bond0.200': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=dhcp + DEVICE=bond0.200 + NM_CONTROLLED=no + ONBOOT=yes + PHYSDEV=bond0 + TYPE=Ethernet + USERCTL=no + VLAN=yes + """).rstrip(' '), + 'ifcfg-br0': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + AGEING=250 + BOOTPROTO=none + DEFROUTE=yes + DEVICE=br0 + IPADDR=192.168.14.2 + IPV6ADDR=2001:1::1 + IPV6INIT=yes + IPV6_DEFAULTGW=2001:4800:78ff:1b::1 + NETMASK=255.255.255.0 + NM_CONTROLLED=no + ONBOOT=yes + PRIO=22 + STP=off + TYPE=Bridge + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth0': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + DEVICE=eth0 + HWADDR=c0:d6:9f:2c:e8:80 + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth0.101': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + DEFROUTE=yes + DEVICE=eth0.101 + GATEWAY=192.168.0.1 + IPADDR=192.168.0.2 + IPADDR1=192.168.2.10 + MTU=1500 + NETMASK=255.255.255.0 + NETMASK1=255.255.255.0 + NM_CONTROLLED=no + ONBOOT=yes + PHYSDEV=eth0 + TYPE=Ethernet + USERCTL=no + VLAN=yes + """).rstrip(' '), + 'ifcfg-eth1': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + DEVICE=eth1 + HWADDR=aa:d6:9f:2c:e8:80 + MASTER=bond0 + NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth2': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + DEVICE=eth2 + HWADDR=c0:bb:9f:2c:e8:80 + MASTER=bond0 + NM_CONTROLLED=no + ONBOOT=yes + SLAVE=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth3': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + BRIDGE=br0 + DEVICE=eth3 + HWADDR=66:bb:9f:2c:e8:80 + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth4': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=none + BRIDGE=br0 + DEVICE=eth4 + HWADDR=98:bb:9f:2c:e8:80 + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' '), + 'ifcfg-eth5': textwrap.dedent("""\ + # Created by cloud-init on instance boot automatically, do not edit. + # + BOOTPROTO=dhcp + DEVICE=eth5 + HWADDR=98:bb:9f:2c:e8:8a + NM_CONTROLLED=no + ONBOOT=yes + TYPE=Ethernet + USERCTL=no + """).rstrip(' ')}, 'yaml': textwrap.dedent(""" version: 1 config: @@ -1702,6 +1867,43 @@ class TestEniRoundTrip(CiTestCase): expected, [line for line in found if line]) +class TestSysconfigRoundTrip(CiTestCase): + def _render_and_read(self, network_config=None, state=None, dir=None): + if dir is None: + dir = self.tmp_dir() + + if network_config: + ns = network_state.parse_net_config_data(network_config) + elif state: + ns = state + else: + raise ValueError("Expected data or state, got neither") + + renderer = sysconfig.Renderer() + renderer.render_network_state(ns, dir) + return dir2dict(dir) + + def testsimple_render_small(self): + entry = NETWORK_CONFIGS['small'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + expected_sysconfig = entry.get('expected_sysconfig') + for ifcfg_name in expected_sysconfig: + expected_ifcfg = expected_sysconfig.get(ifcfg_name) + ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name + self.assertEqual(expected_ifcfg.splitlines(), + files[ifcfg_path].splitlines()) + + def testsimple_render_all(self): + entry = NETWORK_CONFIGS['all'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + expected_sysconfig = entry.get('expected_sysconfig') + for ifcfg_name in expected_sysconfig: + expected_ifcfg = expected_sysconfig.get(ifcfg_name) + ifcfg_path = '/etc/sysconfig/network-scripts/' + ifcfg_name + self.assertEqual(expected_ifcfg.splitlines(), + files[ifcfg_path].splitlines()) + + class TestNetRenderers(CiTestCase): @mock.patch("cloudinit.net.renderers.sysconfig.available") @mock.patch("cloudinit.net.renderers.eni.available") -- 2.7.4