diff --git a/SOURCES/ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch b/SOURCES/ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch new file mode 100644 index 0000000..120a4f1 --- /dev/null +++ b/SOURCES/ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch @@ -0,0 +1,103 @@ +From 93b48730e201bf374f75a3f71d8d6b28211016ba Mon Sep 17 00:00:00 2001 +From: Eduardo Otubo +Date: Tue, 23 Mar 2021 16:14:16 +0100 +Subject: [PATCH] Fix requiring device-number on EC2 derivatives (#836) + +RH-Author: Eduardo Otubo +RH-MergeRequest: 3: Fix requiring device-number on EC2 derivatives (#836) +RH-Commit: [1/1] f372b10d179a969fcf824db8a39bdea3befc4ef4 (eterell/cloud-init) +RH-Bugzilla: 1942699 +RH-Acked-by: Acked-by: Mohammed Gamal +RH-Acked-by: Acked-by: Vitaly Kuznetsov vkuznets@redhat.com +RH-Acked-by: Acked-by: Cathy Avery cavery@redhat.com + +commit 9bd19645a61586b82e86db6f518dd05c3363b17f +Author: James Falcon +Date: Mon Mar 8 14:09:47 2021 -0600 + + Fix requiring device-number on EC2 derivatives (#836) + + #342 (70dbccbb) introduced the ability to determine route-metrics based on + the `device-number` provided by the EC2 IMDS. Not all datasources that + subclass EC2 will have this attribute, so allow the old behavior if + `device-number` is not present. + + LP: #1917875 + +Signed-off-by: Eduardo Otubo +--- + cloudinit/sources/DataSourceEc2.py | 3 +- + .../unittests/test_datasource/test_aliyun.py | 30 +++++++++++++++++++ + 2 files changed, 32 insertions(+), 1 deletion(-) + +diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py +index 1d09c12a..ce69d1b3 100644 +--- a/cloudinit/sources/DataSourceEc2.py ++++ b/cloudinit/sources/DataSourceEc2.py +@@ -764,13 +764,14 @@ def convert_ec2_metadata_network_config( + netcfg['ethernets'][nic_name] = dev_config + return netcfg + # Apply network config for all nics and any secondary IPv4/v6 addresses ++ nic_idx = 0 + for mac, nic_name in sorted(macs_to_nics.items()): + nic_metadata = macs_metadata.get(mac) + if not nic_metadata: + continue # Not a physical nic represented in metadata + # device-number is zero-indexed, we want it 1-indexed for the + # multiplication on the following line +- nic_idx = int(nic_metadata['device-number']) + 1 ++ nic_idx = int(nic_metadata.get('device-number', nic_idx)) + 1 + dhcp_override = {'route-metric': nic_idx * 100} + dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override, + 'dhcp6': False, +diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py +index b626229e..a57f86a1 100644 +--- a/tests/unittests/test_datasource/test_aliyun.py ++++ b/tests/unittests/test_datasource/test_aliyun.py +@@ -7,6 +7,7 @@ from unittest import mock + + from cloudinit import helpers + from cloudinit.sources import DataSourceAliYun as ay ++from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config + from cloudinit.tests import helpers as test_helpers + + DEFAULT_METADATA = { +@@ -183,6 +184,35 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase): + self.assertEqual(ay.parse_public_keys(public_keys), + public_keys['key-pair-0']['openssh-key']) + ++ def test_route_metric_calculated_without_device_number(self): ++ """Test that route-metric code works without `device-number` ++ ++ `device-number` is part of EC2 metadata, but not supported on aliyun. ++ Attempting to access it will raise a KeyError. ++ ++ LP: #1917875 ++ """ ++ netcfg = convert_ec2_metadata_network_config( ++ {"interfaces": {"macs": { ++ "06:17:04:d7:26:09": { ++ "interface-id": "eni-e44ef49e", ++ }, ++ "06:17:04:d7:26:08": { ++ "interface-id": "eni-e44ef49f", ++ } ++ }}}, ++ macs_to_nics={ ++ '06:17:04:d7:26:09': 'eth0', ++ '06:17:04:d7:26:08': 'eth1', ++ } ++ ) ++ ++ met0 = netcfg['ethernets']['eth0']['dhcp4-overrides']['route-metric'] ++ met1 = netcfg['ethernets']['eth1']['dhcp4-overrides']['route-metric'] ++ ++ # route-metric numbers should be 100 apart ++ assert 100 == abs(met0 - met1) ++ + + class TestIsAliYun(test_helpers.CiTestCase): + ALIYUN_PRODUCT = 'Alibaba Cloud ECS' +-- +2.27.0 + diff --git a/SOURCES/ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch b/SOURCES/ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch new file mode 100644 index 0000000..e9a0426 --- /dev/null +++ b/SOURCES/ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch @@ -0,0 +1,150 @@ +From a0601a472dc5b05106617b35b81d8a0578ade339 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= +Date: Thu, 29 Oct 2020 14:38:56 +0100 +Subject: [PATCH 1/2] get_interfaces: don't exclude Open vSwitch bridge/bond + members (#608) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RH-Author: Eduardo Otubo (otubo) +RH-MergeRequest: 6: Patch series to fix "Bug 1957135 - Intermittent failure to start cloud-init due to failure to detect macs" +RH-Commit: [1/2] 4362f855d2d1a250a7d18490b35e65a1133a00c2 (otubo/cloud-init) +RH-Bugzilla: 1957135 +RH-Acked-by: Mohammed Gamal +RH-Acked-by: Emanuele Giuseppe Esposito <[eesposit@redhat.com](mailto:eesposit@redhat.com> + +commit 3c432b32de1bdce2699525201396a8bbc6a41f3e +Author: Lukas Märdian +Date: Thu Oct 29 14:38:56 2020 +0100 + + get_interfaces: don't exclude Open vSwitch bridge/bond members (#608) + + If an OVS bridge was used as the only/primary interface, the 'init' + stage failed with a "Not all expected physical devices present" error, + leaving the system with a broken SSH setup. + + LP: #1898997 + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/__init__.py | 15 +++++++++++-- + cloudinit/net/tests/test_init.py | 36 +++++++++++++++++++++++++++++++- + tools/.github-cla-signers | 1 + + 3 files changed, 49 insertions(+), 3 deletions(-) + +diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py +index e233149a..0aa58b27 100644 +--- a/cloudinit/net/__init__.py ++++ b/cloudinit/net/__init__.py +@@ -124,6 +124,15 @@ def master_is_bridge_or_bond(devname): + return (os.path.exists(bonding_path) or os.path.exists(bridge_path)) + + ++def master_is_openvswitch(devname): ++ """Return a bool indicating if devname's master is openvswitch""" ++ master_path = get_master(devname) ++ if master_path is None: ++ return False ++ ovs_path = sys_dev_path(devname, path="upper_ovs-system") ++ return os.path.exists(ovs_path) ++ ++ + 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 +@@ -855,8 +864,10 @@ def get_interfaces(): + continue + if is_bond(name): + continue +- if get_master(name) is not None and not master_is_bridge_or_bond(name): +- continue ++ if get_master(name) is not None: ++ if (not master_is_bridge_or_bond(name) and ++ not master_is_openvswitch(name)): ++ continue + if is_netfailover(name): + continue + mac = get_interface_mac(name) +diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py +index 311ab6f8..0535387a 100644 +--- a/cloudinit/net/tests/test_init.py ++++ b/cloudinit/net/tests/test_init.py +@@ -190,6 +190,28 @@ class TestReadSysNet(CiTestCase): + self.assertTrue(net.master_is_bridge_or_bond('eth1')) + self.assertTrue(net.master_is_bridge_or_bond('eth2')) + ++ def test_master_is_openvswitch(self): ++ ovs_mac = 'bb:cc:aa:bb:cc:aa' ++ ++ # No master => False ++ write_file(os.path.join(self.sysdir, 'eth1', 'address'), ovs_mac) ++ ++ self.assertFalse(net.master_is_bridge_or_bond('eth1')) ++ ++ # masters without ovs-system => False ++ write_file(os.path.join(self.sysdir, 'ovs-system', 'address'), ovs_mac) ++ ++ os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1', ++ 'master')) ++ ++ self.assertFalse(net.master_is_openvswitch('eth1')) ++ ++ # masters with ovs-system => True ++ os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1', ++ 'upper_ovs-system')) ++ ++ self.assertTrue(net.master_is_openvswitch('eth1')) ++ + def test_is_vlan(self): + """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan.""" + ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent')) +@@ -465,20 +487,32 @@ class TestGetInterfaceMAC(CiTestCase): + ): + bridge_mac = 'aa:bb:cc:aa:bb:cc' + bond_mac = 'cc:bb:aa:cc:bb:aa' ++ ovs_mac = 'bb:cc:aa:bb:cc:aa' ++ + write_file(os.path.join(self.sysdir, 'br0', 'address'), bridge_mac) + write_file(os.path.join(self.sysdir, 'br0', 'bridge'), '') + + write_file(os.path.join(self.sysdir, 'bond0', 'address'), bond_mac) + write_file(os.path.join(self.sysdir, 'bond0', 'bonding'), '') + ++ write_file(os.path.join(self.sysdir, 'ovs-system', 'address'), ++ ovs_mac) ++ + write_file(os.path.join(self.sysdir, 'eth1', 'address'), bridge_mac) + os.symlink('../br0', os.path.join(self.sysdir, 'eth1', 'master')) + + write_file(os.path.join(self.sysdir, 'eth2', 'address'), bond_mac) + os.symlink('../bond0', os.path.join(self.sysdir, 'eth2', 'master')) + ++ write_file(os.path.join(self.sysdir, 'eth3', 'address'), ovs_mac) ++ os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3', ++ 'master')) ++ os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3', ++ 'upper_ovs-system')) ++ + interface_names = [interface[0] for interface in net.get_interfaces()] +- self.assertEqual(['eth1', 'eth2'], sorted(interface_names)) ++ self.assertEqual(['eth1', 'eth2', 'eth3', 'ovs-system'], ++ sorted(interface_names)) + + + class TestInterfaceHasOwnMAC(CiTestCase): +diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers +index e5d2b95c..db55361a 100644 +--- a/tools/.github-cla-signers ++++ b/tools/.github-cla-signers +@@ -16,6 +16,7 @@ matthewruffell + nishigori + omBratteng + onitake ++slyon + smoser + sshedi + TheRealFalcon +-- +2.27.0 + diff --git a/SOURCES/ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch b/SOURCES/ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch new file mode 100644 index 0000000..7304f89 --- /dev/null +++ b/SOURCES/ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch @@ -0,0 +1,512 @@ +From 83e17432645b9e959c82ffe9c86d20fa183bc5ef Mon Sep 17 00:00:00 2001 +From: Daniel Watkins +Date: Mon, 8 Mar 2021 12:50:57 -0500 +Subject: [PATCH 2/2] net: exclude OVS internal interfaces in get_interfaces + (#829) + +RH-Author: Eduardo Otubo (otubo) +RH-MergeRequest: 6: Patch series to fix "Bug 1957135 - Intermittent failure to start cloud-init due to failure to detect macs" +RH-Commit: [2/2] d401dc64a7ceeecb091a792aa24de334940a3750 (otubo/cloud-init) +RH-Bugzilla: 1957135 +RH-Acked-by: Mohammed Gamal +RH-Acked-by: Emanuele Giuseppe Esposito <[eesposit@redhat.com](mailto:eesposit@redhat.com> + +commit 121bc04cdf0e6732fe143b7419131dc250c13384 +Author: Daniel Watkins +Date: Mon Mar 8 12:50:57 2021 -0500 + + net: exclude OVS internal interfaces in get_interfaces (#829) + + `get_interfaces` is used to in two ways, broadly: firstly, to determine + the available interfaces when converting cloud network configuration + formats to cloud-init's network configuration formats; and, secondly, to + ensure that any interfaces which are specified in network configuration + are (a) available, and (b) named correctly. The first of these is + unaffected by this commit, as no clouds support Open vSwitch + configuration in their network configuration formats. + + For the second, we check that MAC addresses of physical devices are + unique. In some OVS configurations, there are OVS-created devices which + have duplicate MAC addresses, either with each other or with physical + devices. As these interfaces are created by OVS, we can be confident + that (a) they will be available when appropriate, and (b) that OVS will + name them correctly. As such, this commit excludes any OVS-internal + interfaces from the set of interfaces returned by `get_interfaces`. + + LP: #1912844 + +Signed-off-by: Eduardo Otubo +--- + cloudinit/net/__init__.py | 62 +++++++++ + cloudinit/net/tests/test_init.py | 119 ++++++++++++++++++ + .../sources/helpers/tests/test_openstack.py | 5 + + cloudinit/sources/tests/test_oracle.py | 4 + + .../integration_tests/bugs/test_lp1912844.py | 103 +++++++++++++++ + .../test_datasource/test_configdrive.py | 8 ++ + tests/unittests/test_net.py | 20 +++ + 7 files changed, 321 insertions(+) + create mode 100644 tests/integration_tests/bugs/test_lp1912844.py + +diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py +index 0aa58b27..2ff770e1 100644 +--- a/cloudinit/net/__init__.py ++++ b/cloudinit/net/__init__.py +@@ -6,6 +6,7 @@ + # This file is part of cloud-init. See LICENSE file for license information. + + import errno ++import functools + import ipaddress + import logging + import os +@@ -19,6 +20,19 @@ from cloudinit.url_helper import UrlError, readurl + LOG = logging.getLogger(__name__) + SYS_CLASS_NET = "/sys/class/net/" + DEFAULT_PRIMARY_INTERFACE = 'eth0' ++OVS_INTERNAL_INTERFACE_LOOKUP_CMD = [ ++ "ovs-vsctl", ++ "--format", ++ "csv", ++ "--no-headings", ++ "--timeout", ++ "10", ++ "--columns", ++ "name", ++ "find", ++ "interface", ++ "type=internal", ++] + + + def natural_sort_key(s, _nsre=re.compile('([0-9]+)')): +@@ -133,6 +147,52 @@ def master_is_openvswitch(devname): + return os.path.exists(ovs_path) + + ++@functools.lru_cache(maxsize=None) ++def openvswitch_is_installed() -> bool: ++ """Return a bool indicating if Open vSwitch is installed in the system.""" ++ ret = bool(subp.which("ovs-vsctl")) ++ if not ret: ++ LOG.debug( ++ "ovs-vsctl not in PATH; not detecting Open vSwitch interfaces" ++ ) ++ return ret ++ ++ ++@functools.lru_cache(maxsize=None) ++def get_ovs_internal_interfaces() -> list: ++ """Return a list of the names of OVS internal interfaces on the system. ++ ++ These will all be strings, and are used to exclude OVS-specific interface ++ from cloud-init's network configuration handling. ++ """ ++ try: ++ out, _err = subp.subp(OVS_INTERNAL_INTERFACE_LOOKUP_CMD) ++ except subp.ProcessExecutionError as exc: ++ if "database connection failed" in exc.stderr: ++ LOG.info( ++ "Open vSwitch is not yet up; no interfaces will be detected as" ++ " OVS-internal" ++ ) ++ return [] ++ raise ++ else: ++ return out.splitlines() ++ ++ ++def is_openvswitch_internal_interface(devname: str) -> bool: ++ """Returns True if this is an OVS internal interface. ++ ++ If OVS is not installed or not yet running, this will return False. ++ """ ++ if not openvswitch_is_installed(): ++ return False ++ ovs_bridges = get_ovs_internal_interfaces() ++ if devname in ovs_bridges: ++ LOG.debug("Detected %s as an OVS interface", devname) ++ return True ++ return False ++ ++ + 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 +@@ -877,6 +937,8 @@ def get_interfaces(): + # skip nics that have no mac (00:00....) + if name != 'lo' and mac == zero_mac[:len(mac)]: + continue ++ if is_openvswitch_internal_interface(name): ++ continue + ret.append((name, mac, device_driver(name), device_devid(name))) + return ret + +diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py +index 0535387a..946f8ee2 100644 +--- a/cloudinit/net/tests/test_init.py ++++ b/cloudinit/net/tests/test_init.py +@@ -391,6 +391,10 @@ class TestGetDeviceList(CiTestCase): + self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist()) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False), ++) + class TestGetInterfaceMAC(CiTestCase): + + def setUp(self): +@@ -1224,6 +1228,121 @@ class TestNetFailOver(CiTestCase): + self.assertFalse(net.is_netfailover(devname, driver)) + + ++class TestOpenvswitchIsInstalled: ++ """Test cloudinit.net.openvswitch_is_installed. ++ ++ Uses the ``clear_lru_cache`` local autouse fixture to allow us to test ++ despite the ``lru_cache`` decorator on the unit under test. ++ """ ++ ++ @pytest.fixture(autouse=True) ++ def clear_lru_cache(self): ++ net.openvswitch_is_installed.cache_clear() ++ ++ @pytest.mark.parametrize( ++ "expected,which_return", [(True, "/some/path"), (False, None)] ++ ) ++ @mock.patch("cloudinit.net.subp.which") ++ def test_mirrors_which_result(self, m_which, expected, which_return): ++ m_which.return_value = which_return ++ assert expected == net.openvswitch_is_installed() ++ ++ @mock.patch("cloudinit.net.subp.which") ++ def test_only_calls_which_once(self, m_which): ++ net.openvswitch_is_installed() ++ net.openvswitch_is_installed() ++ assert 1 == m_which.call_count ++ ++ ++@mock.patch("cloudinit.net.subp.subp", return_value=("", "")) ++class TestGetOVSInternalInterfaces: ++ """Test cloudinit.net.get_ovs_internal_interfaces. ++ ++ Uses the ``clear_lru_cache`` local autouse fixture to allow us to test ++ despite the ``lru_cache`` decorator on the unit under test. ++ """ ++ @pytest.fixture(autouse=True) ++ def clear_lru_cache(self): ++ net.get_ovs_internal_interfaces.cache_clear() ++ ++ def test_command_used(self, m_subp): ++ """Test we use the correct command when we call subp""" ++ net.get_ovs_internal_interfaces() ++ ++ assert [ ++ mock.call(net.OVS_INTERNAL_INTERFACE_LOOKUP_CMD) ++ ] == m_subp.call_args_list ++ ++ def test_subp_contents_split_and_returned(self, m_subp): ++ """Test that the command output is appropriately mangled.""" ++ stdout = "iface1\niface2\niface3\n" ++ m_subp.return_value = (stdout, "") ++ ++ assert [ ++ "iface1", ++ "iface2", ++ "iface3", ++ ] == net.get_ovs_internal_interfaces() ++ ++ def test_database_connection_error_handled_gracefully(self, m_subp): ++ """Test that the error indicating OVS is down is handled gracefully.""" ++ m_subp.side_effect = ProcessExecutionError( ++ stderr="database connection failed" ++ ) ++ ++ assert [] == net.get_ovs_internal_interfaces() ++ ++ def test_other_errors_raised(self, m_subp): ++ """Test that only database connection errors are handled.""" ++ m_subp.side_effect = ProcessExecutionError() ++ ++ with pytest.raises(ProcessExecutionError): ++ net.get_ovs_internal_interfaces() ++ ++ def test_only_runs_once(self, m_subp): ++ """Test that we cache the value.""" ++ net.get_ovs_internal_interfaces() ++ net.get_ovs_internal_interfaces() ++ ++ assert 1 == m_subp.call_count ++ ++ ++@mock.patch("cloudinit.net.get_ovs_internal_interfaces") ++@mock.patch("cloudinit.net.openvswitch_is_installed") ++class TestIsOpenVSwitchInternalInterface: ++ def test_false_if_ovs_not_installed( ++ self, m_openvswitch_is_installed, _m_get_ovs_internal_interfaces ++ ): ++ """Test that OVS' absence returns False.""" ++ m_openvswitch_is_installed.return_value = False ++ ++ assert not net.is_openvswitch_internal_interface("devname") ++ ++ @pytest.mark.parametrize( ++ "detected_interfaces,devname,expected_return", ++ [ ++ ([], "devname", False), ++ (["notdevname"], "devname", False), ++ (["devname"], "devname", True), ++ (["some", "other", "devices", "and", "ours"], "ours", True), ++ ], ++ ) ++ def test_return_value_based_on_detected_interfaces( ++ self, ++ m_openvswitch_is_installed, ++ m_get_ovs_internal_interfaces, ++ detected_interfaces, ++ devname, ++ expected_return, ++ ): ++ """Test that the detected interfaces are used correctly.""" ++ m_openvswitch_is_installed.return_value = True ++ m_get_ovs_internal_interfaces.return_value = detected_interfaces ++ assert expected_return == net.is_openvswitch_internal_interface( ++ devname ++ ) ++ ++ + class TestIsIpAddress: + """Tests for net.is_ip_address. + +diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/cloudinit/sources/helpers/tests/test_openstack.py +index 2bde1e3f..95fb9743 100644 +--- a/cloudinit/sources/helpers/tests/test_openstack.py ++++ b/cloudinit/sources/helpers/tests/test_openstack.py +@@ -1,10 +1,15 @@ + # This file is part of cloud-init. See LICENSE file for license information. + # ./cloudinit/sources/helpers/tests/test_openstack.py ++from unittest import mock + + from cloudinit.sources.helpers import openstack + from cloudinit.tests import helpers as test_helpers + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestConvertNetJson(test_helpers.CiTestCase): + + def test_phy_types(self): +diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py +index 7bd23813..902d1e40 100644 +--- a/cloudinit/sources/tests/test_oracle.py ++++ b/cloudinit/sources/tests/test_oracle.py +@@ -173,6 +173,10 @@ class TestIsPlatformViable(test_helpers.CiTestCase): + m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')]) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestNetworkConfigFromOpcImds: + def test_no_secondary_nics_does_not_mutate_input(self, oracle_ds): + oracle_ds._vnics_data = [{}] +diff --git a/tests/integration_tests/bugs/test_lp1912844.py b/tests/integration_tests/bugs/test_lp1912844.py +new file mode 100644 +index 00000000..efafae50 +--- /dev/null ++++ b/tests/integration_tests/bugs/test_lp1912844.py +@@ -0,0 +1,103 @@ ++"""Integration test for LP: #1912844 ++ ++cloud-init should ignore OVS-internal interfaces when performing its own ++interface determination: these interfaces are handled fully by OVS, so ++cloud-init should never need to touch them. ++ ++This test is a semi-synthetic reproducer for the bug. It uses a similar ++network configuration, tweaked slightly to DHCP in a way that will succeed even ++on "failed" boots. The exact bug doesn't reproduce with the NoCloud ++datasource, because it runs at init-local time (whereas the MAAS datasource, ++from the report, runs only at init (network) time): this means that the ++networking code runs before OVS creates its interfaces (which happens after ++init-local but, of course, before networking is up), and so doesn't generate ++the traceback that they cause. We work around this by calling ++``get_interfaces_by_mac` directly in the test code. ++""" ++import pytest ++ ++from tests.integration_tests import random_mac_address ++ ++MAC_ADDRESS = random_mac_address() ++ ++NETWORK_CONFIG = """\ ++bonds: ++ bond0: ++ interfaces: ++ - enp5s0 ++ macaddress: {0} ++ mtu: 1500 ++bridges: ++ ovs-br: ++ interfaces: ++ - bond0 ++ macaddress: {0} ++ mtu: 1500 ++ openvswitch: {{}} ++ dhcp4: true ++ethernets: ++ enp5s0: ++ mtu: 1500 ++ set-name: enp5s0 ++ match: ++ macaddress: {0} ++version: 2 ++vlans: ++ ovs-br.100: ++ id: 100 ++ link: ovs-br ++ mtu: 1500 ++ ovs-br.200: ++ id: 200 ++ link: ovs-br ++ mtu: 1500 ++""".format(MAC_ADDRESS) ++ ++ ++SETUP_USER_DATA = """\ ++#cloud-config ++packages: ++- openvswitch-switch ++""" ++ ++ ++@pytest.fixture ++def ovs_enabled_session_cloud(session_cloud): ++ """A session_cloud wrapper, to use an OVS-enabled image for tests. ++ ++ This implementation is complicated by wanting to use ``session_cloud``s ++ snapshot cleanup/retention logic, to avoid having to reimplement that here. ++ """ ++ old_snapshot_id = session_cloud.snapshot_id ++ with session_cloud.launch( ++ user_data=SETUP_USER_DATA, ++ ) as instance: ++ instance.instance.clean() ++ session_cloud.snapshot_id = instance.snapshot() ++ ++ yield session_cloud ++ ++ try: ++ session_cloud.delete_snapshot() ++ finally: ++ session_cloud.snapshot_id = old_snapshot_id ++ ++ ++@pytest.mark.lxd_vm ++def test_get_interfaces_by_mac_doesnt_traceback(ovs_enabled_session_cloud): ++ """Launch our OVS-enabled image and confirm the bug doesn't reproduce.""" ++ launch_kwargs = { ++ "config_dict": { ++ "user.network-config": NETWORK_CONFIG, ++ "volatile.eth0.hwaddr": MAC_ADDRESS, ++ }, ++ } ++ with ovs_enabled_session_cloud.launch( ++ launch_kwargs=launch_kwargs, ++ ) as client: ++ result = client.execute( ++ "python3 -c" ++ "'from cloudinit.net import get_interfaces_by_mac;" ++ "get_interfaces_by_mac()'" ++ ) ++ assert result.ok +diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py +index 6f830cc6..2e2b7847 100644 +--- a/tests/unittests/test_datasource/test_configdrive.py ++++ b/tests/unittests/test_datasource/test_configdrive.py +@@ -494,6 +494,10 @@ class TestConfigDriveDataSource(CiTestCase): + self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestNetJson(CiTestCase): + def setUp(self): + super(TestNetJson, self).setUp() +@@ -654,6 +658,10 @@ class TestNetJson(CiTestCase): + self.assertEqual(out_data, conv_data) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestConvertNetworkData(CiTestCase): + + with_logs = True +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 844d5ba8..3607c5e3 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -2825,6 +2825,10 @@ iface eth1 inet dhcp + self.assertEqual(0, mock_settle.call_count) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestRhelSysConfigRendering(CiTestCase): + + with_logs = True +@@ -3495,6 +3499,10 @@ USERCTL=no + expected, self._render_and_read(network_config=v2data)) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestOpenSuseSysConfigRendering(CiTestCase): + + with_logs = True +@@ -4859,6 +4867,10 @@ class TestNetRenderers(CiTestCase): + self.assertTrue(result) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestGetInterfaces(CiTestCase): + _data = {'bonds': ['bond1'], + 'bridges': ['bridge1'], +@@ -5008,6 +5020,10 @@ class TestInterfaceHasOwnMac(CiTestCase): + self.assertFalse(interface_has_own_mac("eth0")) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestGetInterfacesByMac(CiTestCase): + _data = {'bonds': ['bond1'], + 'bridges': ['bridge1'], +@@ -5164,6 +5180,10 @@ class TestInterfacesSorting(CiTestCase): + ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3']) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False) ++) + class TestGetIBHwaddrsByInterface(CiTestCase): + + _ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56' +-- +2.27.0 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index cedad04..da84b6e 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -6,7 +6,7 @@ Name: cloud-init Version: 20.3 -Release: 10%{?dist} +Release: 10%{?dist}.3 Summary: Cloud instance init scripts Group: System Environment/Base @@ -40,6 +40,12 @@ Patch15: ci-DataSourceAzure-update-password-for-defuser-if-exist.patch Patch16: ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch # For bz#1913127 - A typo in cloud-init man page Patch17: ci-fix-a-typo-in-man-page-cloud-init.1-752.patch +# For bz#1942699 - [Aliyun][RHEL8.4][cloud-init] cloud-init service failed to start with Alibaba instance [rhel-8.4.0.z] +Patch18: ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch +# For bz#1957135 - Intermittent failure to start cloud-init due to failure to detect macs [rhel-8.4.0.z] +Patch19: ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch +# For bz#1957135 - Intermittent failure to start cloud-init due to failure to detect macs [rhel-8.4.0.z] +Patch20: ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch BuildArch: noarch @@ -231,6 +237,17 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Thu May 13 2021 Miroslav Rezanina - 20.3-10.el8_4.3 +- ci-get_interfaces-don-t-exclude-Open-vSwitch-bridge-bon.patch [bz#1957135] +- ci-net-exclude-OVS-internal-interfaces-in-get_interface.patch [bz#1957135] +- Resolves: bz#1957135 + (Intermittent failure to start cloud-init due to failure to detect macs [rhel-8.4.0.z]) + +* Tue Apr 06 2021 Miroslav Rezanina - 20.3-10.el8_4.2 +- ci-Fix-requiring-device-number-on-EC2-derivatives-836.patch [bz#1942699] +- Resolves: bz#1942699 + ([Aliyun][RHEL8.4][cloud-init] cloud-init service failed to start with Alibaba instance [rhel-8.4.0.z]) + * Tue Feb 02 2021 Miroslav Rezanina - 20.3-10.el8 - ci-fix-a-typo-in-man-page-cloud-init.1-752.patch [bz#1913127] - Resolves: bz#1913127