From 24bf6147712655fc36a5d714a081853ea37e0312 Mon Sep 17 00:00:00 2001
From: Anh Vo <anhvo@microsoft.com>
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


