Ryan Harper has proposed merging 
~raharper/cloud-init:feature/detect-netfailover into cloud-init:master.

Commit message:
net,Oracle: Add support for netfailover detection

Add support for detecting netfailover[1] device 3-tuple in networking
layer.  In the Oracle datasource ensure that if a provided network
config, either fallback or provided config includes a netfailover master
to remove any MAC address value as this can break under 3-netdev
as the other two devices have the same MAC.

1. https://www.kernel.org/doc/html/latest/networking/net_failover.html

Requested reviews:
  Server Team CI bot (server-team-bot): continuous-integration
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/371895
-- 
Your team cloud-init commiters is requested to review the proposed merge of 
~raharper/cloud-init:feature/detect-netfailover into cloud-init:master.
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index ea707c0..251212c 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -109,6 +109,119 @@ def is_bond(devname):
     return os.path.exists(sys_dev_path(devname, "bonding"))
 
 
+def is_netfailover(devname, driver=None):
+    """ netfailover driver uses 3 nics, master, primary and standby.
+        this returns True if the device is either the primary or standby
+        as these devices are to be ignored.
+    """
+    if driver is None:
+        driver = device_driver(devname)
+    if is_netfail_primary(devname, driver) or is_netfail_standby(devname,
+                                                                 driver):
+        return True
+    return False
+
+
+def get_dev_features(devname):
+    """ Returns a str from reading /sys/class/net/<devname>/device/features."""
+    features = ''
+    try:
+        features = read_sys_net(devname, 'device/features')
+    except Exception:
+        pass
+    return features
+
+
+def has_netfail_standby_feature(devname):
+    """ Return True if the netfail standby feature bit is 63 is set"""
+    features = get_dev_features(devname)
+    if not features or len(features) < 64:
+        return False
+    # array index 62 is the 63'rd bit
+    return features[62] == "1"
+
+
+def is_netfail_master(devname, driver=None):
+    """ A device is a "netfail master" device if:
+
+        - The device driver is 'virtio_net'
+        - The device does NOT have the 'master' sysfs attribute
+        - The device has the standby feature bit set
+
+        Return True if all of the above is True.
+    """
+    if driver is None:
+        driver = device_driver(devname)
+
+    if driver != "virtio_net":
+        return False
+
+    if os.path.exists(sys_dev_path(devname, path='master')):
+        return False
+
+    if not has_netfail_standby_feature(devname):
+        return False
+
+    return True
+
+
+def is_netfail_primary(devname, driver=None):
+    """ A device is a "netfail primary" device if:
+
+        - the device driver is not 'virtio_net'
+        - the device has a 'master' sysfs file
+        - the 'master' sysfs file points to device with virtio_net driver
+        - the 'master' device has the 'standby' feature bit set
+
+        Return True if all of the above is True.
+    """
+    if driver is None:
+        driver = device_driver(devname)
+
+    if driver == "virtio_net":
+        return False
+
+    # /sys/class/net/<devname>/master -> ../../<master devname>
+    master_sysfs_path = sys_dev_path(devname, path='master')
+    if not os.path.exists(master_sysfs_path):
+        return False
+
+    master_devname = os.path.basename(os.path.realpath(master_sysfs_path))
+    master_driver = device_driver(master_devname)
+    if master_driver != "virtio_net":
+        return False
+
+    master_has_standby = has_netfail_standby_feature(master_devname)
+    if not master_has_standby:
+        return False
+
+    return True
+
+
+def is_netfail_standby(devname, driver=None):
+    """ A device is a "netfail standby" device if:
+
+        - The device driver is 'virtio_net'
+        - The device has the standby feature bit set
+        - The device has a 'master' sysfs attribute
+
+        Return True if all of the above is True.
+    """
+    if driver is None:
+        driver = device_driver(devname)
+
+    if driver != "virtio_net":
+        return False
+
+    if not os.path.exists(sys_dev_path(devname, path='master')):
+        return False
+
+    if not has_netfail_standby_feature(devname):
+        return False
+
+    return True
+
+
 def is_renamed(devname):
     """
     /* interface name assignment types (sysfs name_assign_type attribute) */
@@ -227,6 +340,9 @@ def find_fallback_nic(blacklist_drivers=None):
         if is_bond(interface):
             # skip any bonds
             continue
+        if is_netfailover(interface):
+            # ignore netfailover primary/standby interfaces
+            continue
         carrier = read_sys_net_int(interface, 'carrier')
         if carrier:
             connected.append(interface)
@@ -273,9 +389,14 @@ def generate_fallback_config(blacklist_drivers=None, config_driver=None):
     if not target_name:
         # can't read any interfaces addresses (or there are none); give up
         return None
-    target_mac = read_sys_net_safe(target_name, 'address')
-    cfg = {'dhcp4': True, 'set-name': target_name,
-           'match': {'macaddress': target_mac.lower()}}
+
+    # netfail cannot use mac for matching, they have duplicate macs
+    if is_netfail_master(target_name):
+        match = {'name': target_name}
+    else:
+        match = {
+            'macaddress': read_sys_net_safe(target_name, 'address').lower()}
+    cfg = {'dhcp4': True, 'set-name': target_name, 'match': match}
     if config_driver:
         driver = device_driver(target_name)
         if driver:
@@ -661,6 +782,8 @@ def get_interfaces():
             continue
         if is_bond(name):
             continue
+        if is_netfailover(name):
+            continue
         mac = get_interface_mac(name)
         # some devices may not have a mac (tun0)
         if not mac:
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index d2e38f0..7259dbe 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -204,6 +204,10 @@ class TestGenerateFallbackConfig(CiTestCase):
         self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
                        return_value=False)
         self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
+        self.add_patch('cloudinit.net.is_netfailover', 'm_netfail',
+                       return_value=False)
+        self.add_patch('cloudinit.net.is_netfail_master', 'm_netfail_master',
+                       return_value=False)
 
     def test_generate_fallback_finds_connected_eth_with_mac(self):
         """generate_fallback_config finds any connected device with a mac."""
@@ -268,6 +272,61 @@ class TestGenerateFallbackConfig(CiTestCase):
         ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
         self.assertIsNone(net.generate_fallback_config())
 
+    def test_generate_fallback_config_skips_netfail_devs(self):
+        """gen_fallback_config ignores netfail primary,sby no mac on master."""
+        mac = 'aa:bb:cc:aa:bb:cc'  # netfailover devs share the same mac
+        for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
+            write_file(os.path.join(self.sysdir, iface, 'carrier'), '1')
+            write_file(
+                os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
+            write_file(
+                os.path.join(self.sysdir, iface, 'address'), mac)
+
+        def is_netfail(iface, _driver=None):
+            # ens3 is the master
+            if iface == 'ens3':
+                return False
+            return True
+        self.m_netfail.side_effect = is_netfail
+
+        def is_netfail_master(iface, _driver=None):
+            # ens3 is the master
+            if iface == 'ens3':
+                return True
+            return False
+        self.m_netfail_master.side_effect = is_netfail_master
+        expected = {
+            'ethernets': {
+                'ens3': {'dhcp4': True, 'match': {'name': 'ens3'},
+                         'set-name': 'ens3'}},
+            'version': 2}
+        result = net.generate_fallback_config()
+        self.assertEqual(expected, result)
+
+
+class TestNetFindFallBackNic(CiTestCase):
+
+    with_logs = True
+
+    def setUp(self):
+        super(TestNetFindFallBackNic, self).setUp()
+        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
+        self.m_sys_path = sys_mock.start()
+        self.sysdir = self.tmp_dir() + '/'
+        self.m_sys_path.return_value = self.sysdir
+        self.addCleanup(sys_mock.stop)
+        self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
+                       return_value=False)
+        self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
+
+    def test_generate_fallback_finds_first_connected_eth_with_mac(self):
+        """find_fallback_nic finds any connected device with a mac."""
+        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
+        write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
+        mac = 'aa:bb:cc:aa:bb:cc'
+        write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
+        self.assertEqual('eth1', net.find_fallback_nic())
+
 
 class TestGetDeviceList(CiTestCase):
 
@@ -365,6 +424,26 @@ class TestGetInterfaceMAC(CiTestCase):
         expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
         self.assertEqual(expected, net.get_interfaces())
 
+    @mock.patch('cloudinit.net.is_netfailover')
+    def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail):
+        """Ignore interfaces if netfailover primary or standby."""
+        mac = 'aa:bb:cc:aa:bb:cc'  # netfailover devs share the same mac
+        for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
+            write_file(
+                os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
+            write_file(
+                os.path.join(self.sysdir, iface, 'address'), mac)
+
+        def is_netfail(iface, _driver=None):
+            # ens3 is the master
+            if iface == 'ens3':
+                return False
+            else:
+                return True
+        m_netfail.side_effect = is_netfail
+        expected = [('ens3', mac, None, None)]
+        self.assertEqual(expected, net.get_interfaces())
+
 
 class TestInterfaceHasOwnMAC(CiTestCase):
 
@@ -922,3 +1001,234 @@ class TestWaitForPhysdevs(CiTestCase):
         self.m_get_iface_mac.return_value = {}
         net.wait_for_physdevs(netcfg, strict=False)
         self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)
+
+
+class TestNetFailOver(CiTestCase):
+
+    with_logs = True
+
+    def setUp(self):
+        super(TestNetFailOver, self).setUp()
+        self.add_patch('cloudinit.net.util', 'm_util')
+        self.add_patch('cloudinit.net.read_sys_net', 'm_read_sys_net')
+        self.add_patch('cloudinit.net.device_driver', 'm_device_driver')
+
+    def test_get_dev_features(self):
+        devname = self.random_string()
+        features = self.random_string()
+        self.m_read_sys_net.return_value = features
+
+        self.assertEqual(features, net.get_dev_features(devname))
+        self.assertEqual(1, self.m_read_sys_net.call_count)
+        self.assertEqual(mock.call(devname, 'device/features'),
+                         self.m_read_sys_net.call_args_list[0])
+
+    def test_get_dev_features_none_returns_empty_string(self):
+        devname = self.random_string()
+        self.m_read_sys_net.side_effect = Exception('error')
+        self.assertEqual('', net.get_dev_features(devname))
+        self.assertEqual(1, self.m_read_sys_net.call_count)
+        self.assertEqual(mock.call(devname, 'device/features'),
+                         self.m_read_sys_net.call_args_list[0])
+
+    @mock.patch('cloudinit.net.get_dev_features')
+    def test_has_netfail_standby_feature(self, m_dev_features):
+        devname = self.random_string()
+        standby_features = ('0' * 62) + '1' + '0'
+        m_dev_features.return_value = standby_features
+        self.assertTrue(net.has_netfail_standby_feature(devname))
+
+    @mock.patch('cloudinit.net.get_dev_features')
+    def test_has_netfail_standby_feature_short_is_false(self, m_dev_features):
+        devname = self.random_string()
+        standby_features = self.random_string()
+        m_dev_features.return_value = standby_features
+        self.assertFalse(net.has_netfail_standby_feature(devname))
+
+    @mock.patch('cloudinit.net.get_dev_features')
+    def test_has_netfail_standby_feature_not_present_is_false(self,
+                                                              m_dev_features):
+        devname = self.random_string()
+        standby_features = '0' * 64
+        m_dev_features.return_value = standby_features
+        self.assertFalse(net.has_netfail_standby_feature(devname))
+
+    @mock.patch('cloudinit.net.get_dev_features')
+    def test_has_netfail_standby_feature_no_features_is_false(self,
+                                                              m_dev_features):
+        devname = self.random_string()
+        standby_features = None
+        m_dev_features.return_value = standby_features
+        self.assertFalse(net.has_netfail_standby_feature(devname))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_master(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = False  # no master sysfs attr
+        m_standby.return_value = True  # has standby feature flag
+        self.assertTrue(net.is_netfail_master(devname, driver))
+
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_master_checks_master_attr(self, m_sysdev):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_sysdev.return_value = self.random_string()
+        self.assertFalse(net.is_netfail_master(devname, driver))
+        self.assertEqual(1, m_sysdev.call_count)
+        self.assertEqual(mock.call(devname, path='master'),
+                         m_sysdev.call_args_list[0])
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_master_wrong_driver(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()
+        self.assertFalse(net.is_netfail_master(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_master_has_master_attr(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = True  # has master sysfs attr
+        self.assertFalse(net.is_netfail_master(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = False  # no master sysfs attr
+        m_standby.return_value = False  # no standby feature flag
+        self.assertFalse(net.is_netfail_master(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()  # device not virtio_net
+        master_devname = self.random_string()
+        m_sysdev.return_value = "%s/%s" % (self.random_string(),
+                                           master_devname)
+        m_exists.return_value = True  # has master sysfs attr
+        self.m_device_driver.return_value = 'virtio_net'  # master virtio_net
+        m_standby.return_value = True  # has standby feature flag
+        self.assertTrue(net.is_netfail_primary(devname, driver))
+        self.assertEqual(1, self.m_device_driver.call_count)
+        self.assertEqual(mock.call(master_devname),
+                         self.m_device_driver.call_args_list[0])
+        self.assertEqual(1, m_standby.call_count)
+        self.assertEqual(mock.call(master_devname),
+                         m_standby.call_args_list[0])
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_primary_wrong_driver(self, m_sysdev, m_exists,
+                                             m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        self.assertFalse(net.is_netfail_primary(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()  # device not virtio_net
+        m_exists.return_value = False  # no master sysfs attr
+        self.assertFalse(net.is_netfail_primary(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_primary_bad_master(self, m_sysdev, m_exists,
+                                           m_standby):
+        devname = self.random_string()
+        driver = self.random_string()  # device not virtio_net
+        master_devname = self.random_string()
+        m_sysdev.return_value = "%s/%s" % (self.random_string(),
+                                           master_devname)
+        m_exists.return_value = True  # has master sysfs attr
+        self.m_device_driver.return_value = 'XXXX'  # master not virtio_net
+        self.assertFalse(net.is_netfail_primary(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    @mock.patch('cloudinit.net.sys_dev_path')
+    def test_is_netfail_primary_no_standby(self, m_sysdev, m_exists,
+                                           m_standby):
+        devname = self.random_string()
+        driver = self.random_string()  # device not virtio_net
+        master_devname = self.random_string()
+        m_sysdev.return_value = "%s/%s" % (self.random_string(),
+                                           master_devname)
+        m_exists.return_value = True  # has master sysfs attr
+        self.m_device_driver.return_value = 'virtio_net'  # master virtio_net
+        m_standby.return_value = False  # master has no standby feature flag
+        self.assertFalse(net.is_netfail_primary(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_standby(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = True  # has master sysfs attr
+        m_standby.return_value = True  # has standby feature flag
+        self.assertTrue(net.is_netfail_standby(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()
+        self.assertFalse(net.is_netfail_standby(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_standby_no_master(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = False  # has master sysfs attr
+        self.assertFalse(net.is_netfail_standby(devname, driver))
+
+    @mock.patch('cloudinit.net.has_netfail_standby_feature')
+    @mock.patch('cloudinit.net.os.path.exists')
+    def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby):
+        devname = self.random_string()
+        driver = 'virtio_net'
+        m_exists.return_value = True  # has master sysfs attr
+        m_standby.return_value = False  # has standby feature flag
+        self.assertFalse(net.is_netfail_standby(devname, driver))
+
+    @mock.patch('cloudinit.net.is_netfail_standby')
+    @mock.patch('cloudinit.net.is_netfail_primary')
+    def test_is_netfailover_primary(self, m_primary, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()
+        m_primary.return_value = True
+        m_standby.return_value = False
+        self.assertTrue(net.is_netfailover(devname, driver))
+
+    @mock.patch('cloudinit.net.is_netfail_standby')
+    @mock.patch('cloudinit.net.is_netfail_primary')
+    def test_is_netfailover_standby(self, m_primary, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()
+        m_primary.return_value = False
+        m_standby.return_value = True
+        self.assertTrue(net.is_netfailover(devname, driver))
+
+    @mock.patch('cloudinit.net.is_netfail_standby')
+    @mock.patch('cloudinit.net.is_netfail_primary')
+    def test_is_netfailover_returns_false(self, m_primary, m_standby):
+        devname = self.random_string()
+        driver = self.random_string()
+        m_primary.return_value = False
+        m_standby.return_value = False
+        self.assertFalse(net.is_netfailover(devname, driver))
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
index 6e73f56..8651909 100644
--- a/cloudinit/sources/DataSourceOracle.py
+++ b/cloudinit/sources/DataSourceOracle.py
@@ -16,7 +16,7 @@ Notes:
 """
 
 from cloudinit.url_helper import combine_url, readurl, UrlError
-from cloudinit.net import dhcp, get_interfaces_by_mac
+from cloudinit.net import dhcp, get_interfaces_by_mac, is_netfail_master
 from cloudinit import net
 from cloudinit import sources
 from cloudinit import util
@@ -104,6 +104,56 @@ def _add_network_config_from_opc_imds(network_config):
         })
 
 
+def _ensure_netfailover_safe(network_config):
+    """
+    Search network config physical interfaces to see if any of them are
+    a netfailover master.  If found, we prevent matching by MAC as the other
+    failover devices have the same MAC but need to be ignored.
+
+    Note: we rely on cloudinit.net changes which prevent netfailover devices
+    from being present in the provided network config.  For more details about
+    netfailover devices, refer to cloudinit.net module.
+
+    :param network_config
+       A v1 or v2 network config dict with the primary NIC, and possibly
+       secondary nic configured.  This dict will be mutated.
+
+    """
+    # ignore anything that's not an actual network-config
+    if 'version' not in network_config:
+        return
+
+    if network_config['version'] not in [1, 2]:
+        LOG.debug('Ignoring unknown network config version: %s',
+                  network_config['version'])
+        return
+
+    mac_to_name = get_interfaces_by_mac()
+    if network_config['version'] == 1:
+        for cfg in [c for c in network_config['config'] if 'type' in c]:
+            if cfg['type'] == 'physical':
+                if 'mac_address' in cfg:
+                    mac = cfg['mac_address']
+                    cur_name = mac_to_name.get(mac)
+                    if not cur_name:
+                        continue
+                    elif is_netfail_master(cur_name):
+                        del cfg['mac_address']
+
+    elif network_config['version'] == 2:
+        for _, cfg in network_config.get('ethernets', {}).items():
+            if 'match' in cfg:
+                macaddr = cfg.get('match', {}).get('macaddress')
+                if macaddr:
+                    cur_name = mac_to_name.get(macaddr)
+                    if not cur_name:
+                        continue
+                    elif is_netfail_master(cur_name):
+                        del cfg['match']['macaddress']
+                        del cfg['set-name']
+                        cfg['match']['name'] = cur_name
+
+
 class DataSourceOracle(sources.DataSource):
 
     dsname = 'Oracle'
@@ -204,9 +254,13 @@ class DataSourceOracle(sources.DataSource):
         We nonetheless return cmdline provided config if present
         and fallback to generate fallback."""
         if self._network_config == sources.UNSET:
+            # this is v1
             self._network_config = cmdline.read_initramfs_config()
+
             if not self._network_config:
+                # this is now v2
                 self._network_config = self.distro.generate_fallback_config()
+
             if self.ds_cfg.get('configure_secondary_nics'):
                 try:
                     # Mutate self._network_config to include secondary VNICs
@@ -215,6 +269,12 @@ class DataSourceOracle(sources.DataSource):
                     util.logexc(
                         LOG,
                         "Failed to fetch secondary network configuration!")
+
+            # we need to verify that the nic selected is not a netfail over
+            # device and, if it is a netfail master, then we need to avoid
+            # emitting any match by mac
+            _ensure_netfailover_safe(self._network_config)
+
         return self._network_config
 
 
diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
index 3ddf7df..a5298a1 100644
--- a/cloudinit/sources/tests/test_oracle.py
+++ b/cloudinit/sources/tests/test_oracle.py
@@ -8,6 +8,7 @@ from cloudinit.tests import helpers as test_helpers
 
 from textwrap import dedent
 import argparse
+import copy
 import httpretty
 import json
 import mock
@@ -553,4 +554,151 @@ class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
         self.assertEqual('10.0.0.1', subnet_cfg['gateway'])
         self.assertEqual('manual', subnet_cfg['control'])
 
+
+class TestNetworkConfigFiltersNetFailover(test_helpers.CiTestCase):
+
+    with_logs = True
+
+    def setUp(self):
+        super(TestNetworkConfigFiltersNetFailover, self).setUp()
+        self.add_patch(DS_PATH + '.get_interfaces_by_mac',
+                       'm_get_interfaces_by_mac')
+        self.add_patch(DS_PATH + '.is_netfail_master', 'm_netfail_master')
+
+    def test_ignore_bogus_network_config(self):
+        netcfg = {'something': 'here'}
+        passed_netcfg = copy.copy(netcfg)
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+
+    def test_ignore_network_config_unknown_versions(self):
+        netcfg = {'something': 'here', 'version': 3}
+        passed_netcfg = copy.copy(netcfg)
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+
+    def test_checks_v1_type_physical_interfaces(self):
+        mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_addr: nic_name,
+        }
+        netcfg = {'version': 1, 'config': [
+            {'type': 'physical', 'name': nic_name, 'mac_address': mac_addr,
+             'subnets': [{'type': 'dhcp4'}]}]}
+        passed_netcfg = copy.copy(netcfg)
+        self.m_netfail_master.return_value = False
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+        self.assertEqual([mock.call(nic_name)],
+                         self.m_netfail_master.call_args_list)
+
+    def test_checks_v1_skips_non_phys_interfaces(self):
+        mac_addr, nic_name = '00:00:17:02:2b:b1', 'bond0'
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_addr: nic_name,
+        }
+        netcfg = {'version': 1, 'config': [
+            {'type': 'bond', 'name': nic_name, 'mac_address': mac_addr,
+             'subnets': [{'type': 'dhcp4'}]}]}
+        passed_netcfg = copy.copy(netcfg)
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+        self.assertEqual(0, self.m_netfail_master.call_count)
+
+    def test_removes_master_mac_property_v1(self):
+        nic_master, mac_master = 'ens3', self.random_string()
+        nic_other, mac_other = 'ens7', self.random_string()
+        nic_extra, mac_extra = 'enp0s1f2', self.random_string()
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_master: nic_master,
+            mac_other: nic_other,
+            mac_extra: nic_extra,
+        }
+        netcfg = {'version': 1, 'config': [
+            {'type': 'physical', 'name': nic_master,
+             'mac_address': mac_master},
+            {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
+            {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
+        ]}
+
+        def _is_netfail_master(iface):
+            if iface == 'ens3':
+                return True
+            return False
+        self.m_netfail_master.side_effect = _is_netfail_master
+        expected_cfg = {'version': 1, 'config': [
+            {'type': 'physical', 'name': nic_master},
+            {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
+            {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
+        ]}
+        oracle._ensure_netfailover_safe(netcfg)
+        self.assertEqual(expected_cfg, netcfg)
+
+    def test_checks_v2_type_ethernet_interfaces(self):
+        mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_addr: nic_name,
+        }
+        netcfg = {'version': 2, 'ethernets': {
+            nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
+                       'match': {'macaddress': mac_addr}}}}
+        passed_netcfg = copy.copy(netcfg)
+        self.m_netfail_master.return_value = False
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+        self.assertEqual([mock.call(nic_name)],
+                         self.m_netfail_master.call_args_list)
+
+    def test_skips_v2_non_ethernet_interfaces(self):
+        mac_addr, nic_name = '00:00:17:02:2b:b1', 'wlps0'
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_addr: nic_name,
+        }
+        netcfg = {'version': 2, 'wifis': {
+            nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
+                       'match': {'macaddress': mac_addr}}}}
+        passed_netcfg = copy.copy(netcfg)
+        oracle._ensure_netfailover_safe(passed_netcfg)
+        self.assertEqual(netcfg, passed_netcfg)
+        self.assertEqual(0, self.m_netfail_master.call_count)
+
+    def test_removes_master_mac_property_v2(self):
+        nic_master, mac_master = 'ens3', self.random_string()
+        nic_other, mac_other = 'ens7', self.random_string()
+        nic_extra, mac_extra = 'enp0s1f2', self.random_string()
+        self.m_get_interfaces_by_mac.return_value = {
+            mac_master: nic_master,
+            mac_other: nic_other,
+            mac_extra: nic_extra,
+        }
+        netcfg = {'version': 2, 'ethernets': {
+            nic_extra: {'dhcp4': True, 'set-name': nic_extra,
+                        'match': {'macaddress': mac_extra}},
+            nic_other: {'dhcp4': True, 'set-name': nic_other,
+                        'match': {'macaddress': mac_other}},
+            nic_master: {'dhcp4': True, 'set-name': nic_master,
+                         'match': {'macaddress': mac_master}},
+        }}
+
+        def _is_netfail_master(iface):
+            if iface == 'ens3':
+                return True
+            return False
+        self.m_netfail_master.side_effect = _is_netfail_master
+
+        expected_cfg = {'version': 2, 'ethernets': {
+            nic_master: {'dhcp4': True, 'match': {'name': nic_master}},
+            nic_extra: {'dhcp4': True, 'set-name': nic_extra,
+                        'match': {'macaddress': mac_extra}},
+            nic_other: {'dhcp4': True, 'set-name': nic_other,
+                        'match': {'macaddress': mac_other}},
+        }}
+        oracle._ensure_netfailover_safe(netcfg)
+        import pprint
+        pprint.pprint(netcfg)
+        print('---- ^^ modified ^^ ---- vv original vv ----')
+        pprint.pprint(expected_cfg)
+        self.assertEqual(expected_cfg, netcfg)
+
+
 # vi: ts=4 expandtab
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 23fddd0..4dad2af 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -6,7 +6,9 @@ import functools
 import httpretty
 import logging
 import os
+import random
 import shutil
+import string
 import sys
 import tempfile
 import time
@@ -243,6 +245,12 @@ class CiTestCase(TestCase):
             myds.metadata.update(metadata)
         return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)
 
+    @classmethod
+    def random_string(cls, length=8):
+        """ return a random lowercase string with default length of 8"""
+        return ''.join(
+            random.choice(string.ascii_lowercase) for _ in range(length))
+
 
 class ResourceUsingTestCase(CiTestCase):
 
_______________________________________________
Mailing list: https://launchpad.net/~cloud-init-dev
Post to     : cloud-init-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~cloud-init-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to