From 24bf6147712655fc36a5d714a081853ea37e0312 Mon Sep 17 00:00:00 2001 From: Anh Vo Date: Fri, 18 Nov 2022 14:31:27 -0500 Subject: [PATCH] net: skip duplicate mac check for netvsc nic and its VF (#1853) Reference:https://github.com/canonical/cloud-init/commit/24bf6147712655fc36a5d714a081853ea37e0312 Conflict:format diff. When accelerated network is enabled on Azure, the host presents two network interfaces with the same mac address to the VM: a synthetic nic (netvsc) and a VF nic, which is enslaved to the synthetic nic. The net module is already excluding slave nics when enumerating interfaces. However, if cloud-init starts enumerating after the kernel makes the VF visible to userspace, but before the enslaving has finished, cloud-init will see two nics with duplicate mac. This change will skip the duplicate mac error if one of the two nics with duplicate mac is a netvsc nic LP: #1844191 --- cloudinit/net/__init__.py | 39 +++++++++++++++++++++++++++++++++---- tests/unittests/test_net.py | 32 ++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index fba133e..96ce6f5 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -896,13 +896,44 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict: Bridges and any devices that have a 'stolen' mac are excluded.""" ret = {} - for name, mac, _driver, _devid in get_interfaces( + driver_map: dict = {} + for name, mac, driver, _devid in get_interfaces( blacklist_drivers=blacklist_drivers): if mac in ret: - raise RuntimeError( - "duplicate mac found! both '%s' and '%s' have mac '%s'" % - (name, ret[mac], mac)) + raise_duplicate_mac_error = True + msg = "duplicate mac found! both '%s' and '%s' have mac '%s'." % ( + name, + ret[mac], + mac, + ) + # Hyper-V netvsc driver will register a VF with the same mac + # + # The VF will be enslaved to the master nic shortly after + # registration. If cloud-init starts enumerating the interfaces + # before the completion of the enslaving process, it will see + # two different nics with duplicate mac. Cloud-init should ignore + # the slave nic (which does not have hv_netvsc driver). + if driver != driver_map[mac]: + if driver_map[mac] == "hv_netvsc": + LOG.warning( + msg + " Ignoring '%s' due to driver '%s' and " + "'%s' having driver hv_netvsc." + % (name, driver, ret[mac]) + ) + continue + if driver == "hv_netvsc": + raise_duplicate_mac_error = False + LOG.warning( + msg + " Ignoring '%s' due to driver '%s' and " + "'%s' having driver hv_netvsc." + % (ret[mac], driver_map[mac], name) + ) + + if raise_duplicate_mac_error: + raise RuntimeError(msg) + ret[mac] = name + driver_map[mac] = driver # Pretend that an Infiniband GUID is an ethernet address for Openstack # configuration purposes diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index ce19498..0db4442 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -5519,7 +5519,8 @@ class TestGetInterfacesByMac(CiTestCase): 'bridges': ['bridge1'], 'vlans': ['bond1.101'], 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1', - 'bond1.101', 'lo'], + 'bond1.101', 'lo', + "netvsc0-vf", "netvsc0", "netvsc1","netvsc1-vf"], 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01', 'enp0s2': 'aa:aa:aa:aa:aa:02', 'bond1': 'aa:aa:aa:aa:aa:01', @@ -5528,12 +5529,27 @@ class TestGetInterfacesByMac(CiTestCase): 'bridge1-nic': 'aa:aa:aa:aa:aa:03', 'lo': '00:00:00:00:00:00', 'greptap0': '00:00:00:00:00:00', - 'tun0': None}} + "greptap0": "00:00:00:00:00:00", + "netvsc0-vf": "aa:aa:aa:aa:aa:04", + "netvsc0": "aa:aa:aa:aa:aa:04", + "netvsc1-vf": "aa:aa:aa:aa:aa:05", + "netvsc1": "aa:aa:aa:aa:aa:05", + 'tun0': None}, + "drivers": { + "netvsc0": "hv_netvsc", + "netvsc0-vf": "foo", + "netvsc1": "hv_netvsc", + "netvsc1-vf": "bar", + }, + } data = {} def _se_get_devicelist(self): return list(self.data['devices']) + def _se_device_driver(self, name): + return self.data["drivers"].get(name, None) + def _se_get_interface_mac(self, name): return self.data['macs'][name] @@ -5553,7 +5569,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', + mocks = ('get_devicelist', "device_driver", 'get_interface_mac', 'is_bridge', 'interface_has_own_mac', 'is_vlan', 'get_ib_interface_hwaddr') self.mocks = {} for n in mocks: @@ -5567,6 +5583,11 @@ class TestGetInterfacesByMac(CiTestCase): self.data['macs']['bridge1-nic'] = self.data['macs']['enp0s1'] self.assertRaises(RuntimeError, net.get_interfaces_by_mac) + def test_raise_exception_on_duplicate_netvsc_macs(self): + self._mock_setup() + self.data["macs"]["netvsc0"] = self.data["macs"]["netvsc1"] + self.assertRaises(RuntimeError, net.get_interfaces_by_mac) + def test_excludes_any_without_mac_address(self): self._mock_setup() ret = net.get_interfaces_by_mac() @@ -5580,7 +5601,10 @@ 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', '00:00:00:00:00:00': 'lo'}, + 'aa:aa:aa:aa:aa:03': 'bridge1-nic', '00:00:00:00:00:00': 'lo', + "aa:aa:aa:aa:aa:04": "netvsc0", + "aa:aa:aa:aa:aa:05": "netvsc1", + }, ret) def test_excludes_bridges(self): -- 2.33.0