From b954b98a1c25b8db753dcd4545e2a72bbd0a2790 Mon Sep 17 00:00:00 2001 From: Neal Gompa Date: Tue, 22 Feb 2022 07:30:58 -0500 Subject: [PATCH] Rebase to 22.1 - Backport cloud-init PR to add proper NetworkManager support - Add patch to prefer NetworkManager --- cloud-init-21.3-disable-lxd-tests.patch | 33 - cloud-init-21.3-nm-controlled.patch | 500 ---- ...nit-21.3-no-override-default-network.patch | 36 - ...x-unit-test-broken-by-pyyaml-upgrade.patch | 34 - cloud-init-22.1-PR1224-full-nm-support.patch | 2442 +++++++++++++++++ cloud-init-22.1-nm-default.patch | 48 + cloud-init.spec | 47 +- sources | 2 +- 8 files changed, 2517 insertions(+), 625 deletions(-) delete mode 100644 cloud-init-21.3-disable-lxd-tests.patch delete mode 100644 cloud-init-21.3-nm-controlled.patch delete mode 100644 cloud-init-21.3-no-override-default-network.patch delete mode 100644 cloud-init-21.4-Fix-unit-test-broken-by-pyyaml-upgrade.patch create mode 100644 cloud-init-22.1-PR1224-full-nm-support.patch create mode 100644 cloud-init-22.1-nm-default.patch diff --git a/cloud-init-21.3-disable-lxd-tests.patch b/cloud-init-21.3-disable-lxd-tests.patch deleted file mode 100644 index 4a07063..0000000 --- a/cloud-init-21.3-disable-lxd-tests.patch +++ /dev/null @@ -1,33 +0,0 @@ -From f70a9a0a98c0af5a7b2aea9a8e4b40bbe1668038 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 3 Dec 2020 12:31:42 +0100 -Subject: [PATCH 1/3] Disable LXD tests - -Signed-off-by: Eduardo Otubo ---- - tests/cloud_tests/platforms/__init__.py | 2 -- - 1 file changed, 2 deletions(-) - -diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py -index e506baa0..e7efcba5 100644 ---- a/tests/cloud_tests/platforms/__init__.py -+++ b/tests/cloud_tests/platforms/__init__.py -@@ -3,7 +3,6 @@ - """Main init.""" - - from .ec2 import platform as ec2 --from .lxd import platform as lxd - from .nocloudkvm import platform as nocloudkvm - from .azurecloud import platform as azurecloud - from ..util import emit_dots_on_travis -@@ -11,7 +10,6 @@ from ..util import emit_dots_on_travis - PLATFORMS = { - 'ec2': ec2.EC2Platform, - 'nocloud-kvm': nocloudkvm.NoCloudKVMPlatform, -- 'lxd': lxd.LXDPlatform, - 'azurecloud': azurecloud.AzureCloudPlatform, - } - --- -2.27.0 - diff --git a/cloud-init-21.3-nm-controlled.patch b/cloud-init-21.3-nm-controlled.patch deleted file mode 100644 index cd3970b..0000000 --- a/cloud-init-21.3-nm-controlled.patch +++ /dev/null @@ -1,500 +0,0 @@ -From 8caf88512cf2c558e55e78b82c27809f5061e2da Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 2 Sep 2021 11:22:14 +0200 -Subject: [PATCH] Do not write NM_CONTROLLED=no in generated interface config - files - -Conflicts 20.3: - - Not appplying patch on cloudinit/net/sysconfig.py since it now has a -mechanism to identify if cloud-init is running on RHEL, having the -correct settings for NM_CONTROLLED. - -X-downstream-only: true -Signed-off-by: Eduardo Otubo -Signed-off-by: Ryan McCabe ---- - cloudinit/net/sysconfig.py | 2 +- - .../unittests/test_distros/test_netconfig.py | 8 --- - tests/unittests/test_net.py | 49 ------------------- - 3 files changed, 1 insertion(+), 58 deletions(-) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 06f7255e..ccbd785a 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -290,7 +290,7 @@ class Renderer(renderer.Renderer): - # details about this) - - iface_defaults = { -- 'rhel': {'ONBOOT': True, 'USERCTL': False, 'NM_CONTROLLED': False, -+ 'rhel': {'ONBOOT': True, 'USERCTL': False, - 'BOOTPROTO': 'none'}, - 'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'}, - } -diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py -index d09e46af..3f01c99e 100644 ---- a/tests/unittests/test_distros/test_netconfig.py -+++ b/tests/unittests/test_distros/test_netconfig.py -@@ -503,7 +503,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - GATEWAY=192.168.1.254 - IPADDR=192.168.1.5 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -511,7 +510,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - self.ifcfg_path('eth1'): dedent("""\ - BOOTPROTO=dhcp - DEVICE=eth1 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -536,7 +534,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - IPV6_AUTOCONF=no - IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 - IPV6_FORCE_ACCEPT_RA=no -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -544,7 +541,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - self.ifcfg_path('eth1'): dedent("""\ - BOOTPROTO=dhcp - DEVICE=eth1 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -578,7 +574,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - HWADDR=00:16:3e:60:7c:df - IPADDR=192.10.1.2 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -588,7 +583,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - DEVICE=infra0 - IPADDR=10.0.1.2 - NETMASK=255.255.0.0 -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=eth0 - USERCTL=no -@@ -617,7 +611,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - DEVICE=eth0 - IPADDR=192.10.1.2 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -627,7 +620,6 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase): - DEVICE=eth0.1001 - IPADDR=10.0.1.2 - NETMASK=255.255.0.0 -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=eth0 - USERCTL=no -diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py -index fc77b11e..3e35ff4d 100644 ---- a/tests/unittests/test_net.py -+++ b/tests/unittests/test_net.py -@@ -535,7 +535,6 @@ GATEWAY=172.19.3.254 - HWADDR=fa:16:3e:ed:9a:59 - IPADDR=172.19.1.34 - NETMASK=255.255.252.0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -633,7 +632,6 @@ IPADDR=172.19.1.34 - IPADDR1=10.0.0.10 - NETMASK=255.255.252.0 - NETMASK1=255.255.255.0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -756,7 +754,6 @@ IPV6_AUTOCONF=no - IPV6_DEFAULTGW=2001:DB8::1 - IPV6_FORCE_ACCEPT_RA=no - NETMASK=255.255.252.0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -910,7 +907,6 @@ NETWORK_CONFIGS = { - BOOTPROTO=none - DEVICE=eth1 - HWADDR=cf:d6:af:48:e8:80 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -927,7 +923,6 @@ NETWORK_CONFIGS = { - IPADDR=192.168.21.3 - NETMASK=255.255.255.0 - METRIC=10000 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -1075,7 +1070,6 @@ NETWORK_CONFIGS = { - IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=no - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1144,7 +1138,6 @@ NETWORK_CONFIGS = { - DHCPV6C=yes - IPV6INIT=yes - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1199,7 +1192,6 @@ NETWORK_CONFIGS = { - IPV6INIT=yes - IPV6_FORCE_ACCEPT_RA=yes - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1254,7 +1246,6 @@ NETWORK_CONFIGS = { - IPV6INIT=yes - IPV6_FORCE_ACCEPT_RA=no - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1299,7 +1290,6 @@ NETWORK_CONFIGS = { - IPV6_AUTOCONF=yes - IPV6INIT=yes - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1326,7 +1316,6 @@ NETWORK_CONFIGS = { - IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=no - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1373,7 +1362,6 @@ NETWORK_CONFIGS = { - IPV6_AUTOCONF=yes - IPV6INIT=yes - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1421,7 +1409,6 @@ NETWORK_CONFIGS = { - IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=yes - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1454,7 +1441,6 @@ NETWORK_CONFIGS = { - 'ifcfg-iface0': textwrap.dedent("""\ - BOOTPROTO=dhcp - DEVICE=iface0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1497,7 +1483,6 @@ NETWORK_CONFIGS = { - BOOTPROTO=dhcp - DEVICE=iface0 - ETHTOOL_OPTS="wol g" -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -1786,7 +1771,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - DHCPV6C=yes - IPV6INIT=yes - MACADDR=aa:bb:cc:dd:ee:ff -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Bond - USERCTL=no"""), -@@ -1794,7 +1778,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BOOTPROTO=dhcp - DEVICE=bond0.200 - DHCLIENT_SET_DEFAULT_ROUTE=no -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=bond0 - USERCTL=no -@@ -1812,7 +1795,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - IPV6_DEFAULTGW=2001:4800:78ff:1b::1 - MACADDR=bb:bb:bb:bb:bb:aa - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - PRIO=22 - STP=no -@@ -1822,7 +1804,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BOOTPROTO=none - DEVICE=eth0 - HWADDR=c0:d6:9f:2c:e8:80 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -1839,7 +1820,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - MTU=1500 - NETMASK=255.255.255.0 - NETMASK1=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=eth0 - USERCTL=no -@@ -1849,7 +1829,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - DEVICE=eth1 - HWADDR=aa:d6:9f:2c:e8:80 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Ethernet -@@ -1859,7 +1838,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - DEVICE=eth2 - HWADDR=c0:bb:9f:2c:e8:80 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Ethernet -@@ -1869,7 +1847,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BRIDGE=br0 - DEVICE=eth3 - HWADDR=66:bb:9f:2c:e8:80 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -1878,7 +1855,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - BRIDGE=br0 - DEVICE=eth4 - HWADDR=98:bb:9f:2c:e8:80 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -1887,7 +1863,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - DEVICE=eth5 - DHCLIENT_SET_DEFAULT_ROUTE=no - HWADDR=98:bb:9f:2c:e8:8a -- NM_CONTROLLED=no - ONBOOT=no - TYPE=Ethernet - USERCTL=no"""), -@@ -1898,7 +1873,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true - IPADDR=192.168.200.7 - MTU=9000 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=InfiniBand - USERCTL=no"""), -@@ -2343,7 +2317,6 @@ iface bond0 inet6 static - MTU=9000 - NETMASK=255.255.255.0 - NETMASK1=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Bond - USERCTL=no -@@ -2353,7 +2326,6 @@ iface bond0 inet6 static - DEVICE=bond0s0 - HWADDR=aa:bb:cc:dd:e8:00 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Ethernet -@@ -2375,7 +2347,6 @@ iface bond0 inet6 static - DEVICE=bond0s1 - HWADDR=aa:bb:cc:dd:e8:01 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Ethernet -@@ -2432,7 +2403,6 @@ iface bond0 inet6 static - BOOTPROTO=none - DEVICE=en0 - HWADDR=aa:bb:cc:dd:e8:00 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no"""), -@@ -2451,7 +2421,6 @@ iface bond0 inet6 static - MTU=2222 - NETMASK=255.255.255.0 - NETMASK1=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=en0 - USERCTL=no -@@ -2516,7 +2485,6 @@ iface bond0 inet6 static - DEVICE=br0 - IPADDR=192.168.2.2 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - PRIO=22 - STP=no -@@ -2532,7 +2500,6 @@ iface bond0 inet6 static - IPV6INIT=yes - IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=no -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2546,7 +2513,6 @@ iface bond0 inet6 static - IPV6INIT=yes - IPV6_AUTOCONF=no - IPV6_FORCE_ACCEPT_RA=no -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2640,7 +2606,6 @@ iface bond0 inet6 static - HWADDR=52:54:00:12:34:00 - IPADDR=192.168.1.2 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=no - TYPE=Ethernet - USERCTL=no -@@ -2650,7 +2615,6 @@ iface bond0 inet6 static - DEVICE=eth1 - HWADDR=52:54:00:12:34:aa - MTU=1480 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -2659,7 +2623,6 @@ iface bond0 inet6 static - BOOTPROTO=none - DEVICE=eth2 - HWADDR=52:54:00:12:34:ff -- NM_CONTROLLED=no - ONBOOT=no - TYPE=Ethernet - USERCTL=no -@@ -3080,7 +3043,6 @@ class TestRhelSysConfigRendering(CiTestCase): - BOOTPROTO=dhcp - DEVICE=eth1000 - HWADDR=07-1c-c6-75-a4-be --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3201,7 +3163,6 @@ GATEWAY=10.0.2.2 - HWADDR=52:54:00:12:34:00 - IPADDR=10.0.2.15 - NETMASK=255.255.255.0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3232,7 +3193,6 @@ HWADDR=fa:16:3e:25:b4:59 - IPADDR=51.68.89.122 - MTU=1500 - NETMASK=255.255.240.0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3246,7 +3206,6 @@ DEVICE=eth1 - DHCLIENT_SET_DEFAULT_ROUTE=no - HWADDR=fa:16:3e:b1:ca:29 - MTU=9000 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3271,7 +3230,6 @@ USERCTL=no - # - BOOTPROTO=dhcp - DEVICE=eth0 --NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3546,7 +3504,6 @@ USERCTL=no - IPV6_FORCE_ACCEPT_RA=no - IPV6_DEFAULTGW=2001:db8::1 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3571,7 +3528,6 @@ USERCTL=no - 'ifcfg-eno1': textwrap.dedent("""\ - BOOTPROTO=none - DEVICE=eno1 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no -@@ -3582,7 +3538,6 @@ USERCTL=no - IPADDR=192.6.1.9 - MTU=1495 - NETMASK=255.255.255.0 -- NM_CONTROLLED=no - ONBOOT=yes - PHYSDEV=eno1 - USERCTL=no -@@ -3612,7 +3567,6 @@ USERCTL=no - IPADDR=10.101.8.65 - MTU=1334 - NETMASK=255.255.255.192 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Bond - USERCTL=no -@@ -3622,7 +3576,6 @@ USERCTL=no - BOOTPROTO=none - DEVICE=enp0s0 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Bond -@@ -3633,7 +3586,6 @@ USERCTL=no - BOOTPROTO=none - DEVICE=enp0s1 - MASTER=bond0 -- NM_CONTROLLED=no - ONBOOT=yes - SLAVE=yes - TYPE=Bond -@@ -3657,7 +3609,6 @@ USERCTL=no - DEVICE=eno1 - HWADDR=07-1c-c6-75-a4-be - METRIC=100 -- NM_CONTROLLED=no - ONBOOT=yes - TYPE=Ethernet - USERCTL=no --- -2.27.0 - diff --git a/cloud-init-21.3-no-override-default-network.patch b/cloud-init-21.3-no-override-default-network.patch deleted file mode 100644 index 05a9c89..0000000 --- a/cloud-init-21.3-no-override-default-network.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 1a2b2cf55115a15244b9a7786959415f9fe421f9 Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 2 Sep 2021 10:36:28 +0200 -Subject: [PATCH] Don't override default network configuration - -Signed-off-by: Eduardo Otubo ---- - cloudinit/net/sysconfig.py | 12 +++++++++++- - 1 file changed, 11 insertions(+), 1 deletion(-) - -diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py -index 06f7255e..bbe22d3d 100644 ---- a/cloudinit/net/sysconfig.py -+++ b/cloudinit/net/sysconfig.py -@@ -940,7 +940,17 @@ class Renderer(renderer.Renderer): - # Distros configuring /etc/sysconfig/network as a file e.g. Centos - if sysconfig_path.endswith('network'): - util.ensure_dir(os.path.dirname(sysconfig_path)) -- netcfg = [_make_header(), 'NETWORKING=yes'] -+ # Make sure that existing lines, other than overriding ones, remain -+ netcfg = [] -+ for line in util.load_file(sysconfig_path, quiet=True).split('\n'): -+ if 'cloud-init' in line: -+ break -+ if not line.startswith(('NETWORKING=', -+ 'IPV6_AUTOCONF=', -+ 'NETWORKING_IPV6=')): -+ netcfg.append(line) -+ # Now generate the cloud-init portion of sysconfig/network -+ netcfg.extend([_make_header(), 'NETWORKING=yes']) - if network_state.use_ipv6: - netcfg.append('NETWORKING_IPV6=yes') - netcfg.append('IPV6_AUTOCONF=no') --- -2.27.0 - diff --git a/cloud-init-21.4-Fix-unit-test-broken-by-pyyaml-upgrade.patch b/cloud-init-21.4-Fix-unit-test-broken-by-pyyaml-upgrade.patch deleted file mode 100644 index c810602..0000000 --- a/cloud-init-21.4-Fix-unit-test-broken-by-pyyaml-upgrade.patch +++ /dev/null @@ -1,34 +0,0 @@ -From 125dcb28ea30affeec44029d99bee4b130d5fdc8 Mon Sep 17 00:00:00 2001 -From: James Falcon -Date: Mon, 18 Oct 2021 10:20:18 -0500 -Subject: [PATCH] Fix unit test broken by pyyaml upgrade (#1071) - -PyYAML upgraded from 5.4.1 to 6.0.0. 6.0.0 always requires a `Loader` -arg to `yaml.load()` ---- - tests/unittests/test_net_freebsd.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py -index 466d472b84..e339e1324b 100644 ---- a/tests/unittests/test_net_freebsd.py -+++ b/tests/unittests/test_net_freebsd.py -@@ -1,8 +1,8 @@ - import os --import yaml - - import cloudinit.net - import cloudinit.net.network_state -+from cloudinit import safeyaml - from cloudinit.tests.helpers import (CiTestCase, mock, readResource, dir2dict) - - -@@ -65,7 +65,7 @@ def test_render_output_has_yaml(self, mock_subp): - entry = { - 'yaml': V1, - } -- network_config = yaml.load(entry['yaml']) -+ network_config = safeyaml.load(entry['yaml']) - ns = cloudinit.net.network_state.parse_net_config_data(network_config) - files = self._render_and_read(state=ns) - assert files == { diff --git a/cloud-init-22.1-PR1224-full-nm-support.patch b/cloud-init-22.1-PR1224-full-nm-support.patch new file mode 100644 index 0000000..827da0c --- /dev/null +++ b/cloud-init-22.1-PR1224-full-nm-support.patch @@ -0,0 +1,2442 @@ +From a60d61ea50e59b40bc1341831c622cac89bcc3df Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Mon, 31 Jan 2022 09:44:03 +0100 +Subject: [PATCH 1/6] Replace invalid IP addresses in test + +My guess is that they're invalid by accident, unless the tests were really +meant to test garbage-in-garbage-out behavior. +--- + tests/unittests/test_net.py | 20 ++++++++++---------- + 1 file changed, 10 insertions(+), 10 deletions(-) + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 47e4ba00b0..61e4fce039 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -2419,10 +2419,10 @@ + - type: static + address: 2001:1::1/92 + routes: +- - gateway: 2001:67c:1562:1 ++ - gateway: 2001:67c:1562::1 + network: 2001:67c:1 + netmask: "ffff:ffff::" +- - gateway: 3001:67c:1562:1 ++ - gateway: 3001:67c:1562::1 + network: 3001:67c:1 + netmask: "ffff:ffff::" + metric: 10000 +@@ -2467,10 +2467,10 @@ + - to: 10.1.3.0/24 + via: 192.168.0.3 + - to: 2001:67c:1/32 +- via: 2001:67c:1562:1 ++ via: 2001:67c:1562::1 + - metric: 10000 + to: 3001:67c:1/32 +- via: 3001:67c:1562:1 ++ via: 3001:67c:1562::1 + """ + ), + "expected_eni": textwrap.dedent( +@@ -2530,11 +2530,11 @@ + # control-alias bond0 + iface bond0 inet6 static + address 2001:1::1/92 +- post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true +- pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true +- post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ ++ post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true ++ pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true ++ post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562::1 metric 10000 \ + || true +- pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \ ++ pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562::1 metric 10000 \ + || true + """ + ), +@@ -2712,8 +2712,8 @@ + """\ + # Created by cloud-init on instance boot automatically, do not edit. + # +- 2001:67c:1/32 via 2001:67c:1562:1 dev bond0 +- 3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0 ++ 2001:67c:1/32 via 2001:67c:1562::1 dev bond0 ++ 3001:67c:1/32 via 3001:67c:1562::1 metric 10000 dev bond0 + """ + ), + "route-bond0": textwrap.dedent( + +From 76f7bb9d8b64a653cb53face69351b059e30bc0b Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Wed, 2 Feb 2022 12:48:32 +0100 +Subject: [PATCH 2/6] Use a tad shorter gateway address in net test + +Now that we fixed it (previous commit) by adding a missing colon, one line +ends up one character longer that flake8 would have preferred it to be. + +Shorten it to appease the all too unforgiving CI gods. +--- + tests/unittests/test_net.py | 18 +++++++++--------- + 1 file changed, 9 insertions(+), 9 deletions(-) + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 61e4fce039..517e13123d 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -2422,7 +2422,7 @@ + - gateway: 2001:67c:1562::1 + network: 2001:67c:1 + netmask: "ffff:ffff::" +- - gateway: 3001:67c:1562::1 ++ - gateway: 3001:67c:15::1 + network: 3001:67c:1 + netmask: "ffff:ffff::" + metric: 10000 +@@ -2470,7 +2470,7 @@ + via: 2001:67c:1562::1 + - metric: 10000 + to: 3001:67c:1/32 +- via: 3001:67c:1562::1 ++ via: 3001:67c:15::1 + """ + ), + "expected_eni": textwrap.dedent( +@@ -2532,9 +2532,9 @@ + address 2001:1::1/92 + post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true + pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true +- post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562::1 metric 10000 \ ++ post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ + || true +- pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562::1 metric 10000 \ ++ pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \ + || true + """ + ), +@@ -2577,8 +2577,8 @@ + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:1562:8007::1/64 +- via: 3001:67c:1562:8007::aac:40b2 ++ to: 3001:67c:15:8007::1/64 ++ via: 3001:67c:15:8007::aac:40b2 + """ + ), + "expected_netplan-v2": textwrap.dedent( +@@ -2610,8 +2610,8 @@ + - to: 2001:67c:1562:8007::1/64 + via: 2001:67c:1562:8007::aac:40b2 + - metric: 10000 +- to: 3001:67c:1562:8007::1/64 +- via: 3001:67c:1562:8007::aac:40b2 ++ to: 3001:67c:15:8007::1/64 ++ via: 3001:67c:15:8007::aac:40b2 + ethernets: + eth0: + match: +@@ -2713,7 +2713,7 @@ + # Created by cloud-init on instance boot automatically, do not edit. + # + 2001:67c:1/32 via 2001:67c:1562::1 dev bond0 +- 3001:67c:1/32 via 3001:67c:1562::1 metric 10000 dev bond0 ++ 3001:67c:1/32 via 3001:67c:15::1 metric 10000 dev bond0 + """ + ), + "route-bond0": textwrap.dedent( + +From df4f8b35f4e469e123349896be0804300a9786af Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Fri, 28 Jan 2022 17:30:13 +0100 +Subject: [PATCH 3/6] Revert "net: Make sysconfig renderer compatible with + Network Manager." + +Firstly, this relies upon the fact that you can get ifcfg support by adding +it to NetworkManager.conf. That is not guarranteed and certianly will not +be case in future. + +Secondly, cloud-init always generates configuration with +NM_CONTROLLED=no, so the generated ifcfg files are no good for +NetworkManager. Fedora patches around this by just removing those lines +in their cloud-init package. + +Let's remove this and add a proper NetworkManager support later on +instead. + +This reverts commit 3861102fcaf47a882516d8b6daab518308eb3086. +--- + cloudinit/net/sysconfig.py | 37 +----------- + tests/unittests/test_net.py | 113 +++++------------------------------- + 2 files changed, 17 insertions(+), 133 deletions(-) + +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index ba85c4f673..d866c9aa79 100644 +--- a/cloudinit/net/sysconfig.py ++++ b/cloudinit/net/sysconfig.py +@@ -5,8 +5,6 @@ + import os + import re + +-from configobj import ConfigObj +- + from cloudinit import log as logging + from cloudinit import subp, util + from cloudinit.distros.parsers import networkmanager_conf, resolv_conf +@@ -66,24 +64,6 @@ def _quote_value(value): + return value + + +-def enable_ifcfg_rh(path): +- """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present""" +- config = ConfigObj(path) +- if "main" in config: +- if "plugins" in config["main"]: +- if "ifcfg-rh" in config["main"]["plugins"]: +- return +- else: +- config["main"]["plugins"] = [] +- +- if isinstance(config["main"]["plugins"], list): +- config["main"]["plugins"].append("ifcfg-rh") +- else: +- config["main"]["plugins"] = [config["main"]["plugins"], "ifcfg-rh"] +- config.write() +- LOG.debug("Enabled ifcfg-rh NetworkManager plugins") +- +- + class ConfigMap(object): + """Sysconfig like dictionary object.""" + +@@ -1032,8 +1012,6 @@ def render_network_state(self, network_state, templates=None, target=None): + netrules_content = self._render_persistent_net(network_state) + netrules_path = subp.target_path(target, self.netrules_path) + util.write_file(netrules_path, netrules_content, file_mode) +- if available_nm(target=target): +- enable_ifcfg_rh(subp.target_path(target, path=NM_CFG_FILE)) + + sysconfig_path = subp.target_path(target, templates.get("control")) + # Distros configuring /etc/sysconfig/network as a file e.g. Centos +@@ -1063,14 +1041,9 @@ def _supported_vlan_names(rdev, vid): + + + def available(target=None): +- sysconfig = available_sysconfig(target=target) +- nm = available_nm(target=target) +- return util.system_info()["variant"] in KNOWN_DISTROS and any( +- [nm, sysconfig] +- ) +- ++ if not util.system_info()["variant"] in KNOWN_DISTROS: ++ return False + +-def available_sysconfig(target=None): + expected = ["ifup", "ifdown"] + search = ["/sbin", "/usr/sbin"] + for p in expected: +@@ -1087,10 +1060,4 @@ def available_sysconfig(target=None): + return False + + +-def available_nm(target=None): +- if not os.path.isfile(subp.target_path(target, path=NM_CFG_FILE)): +- return False +- return True +- +- + # vi: ts=4 expandtab +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 517e13123d..c473c79142 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -3522,7 +3522,6 @@ class TestRhelSysConfigRendering(CiTestCase): + + with_logs = True + +- nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf" + scripts_dir = "/etc/sysconfig/network-scripts" + header = ( + "# Created by cloud-init on instance boot automatically, " +@@ -4100,78 +4099,6 @@ def test_wakeonlan_enabled_config_v2(self): + self._compare_files_to_expected(entry[self.expected_name], found) + self._assert_headers(found) + +- def test_check_ifcfg_rh(self): +- """ifcfg-rh plugin is added NetworkManager.conf if conf present.""" +- render_dir = self.tmp_dir() +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is a list here +- with open(nm_cfg, "w") as fh: +- fh.write("# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n") +- self.assertTrue(os.path.exists(nm_cfg)) +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- +- def test_check_ifcfg_rh_plugins_string(self): +- """ifcfg-rh plugin is append when plugins is a string.""" +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is a value here +- util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\nplugins=foo\n") +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check raw content has plugin +- nm_file_content = util.load_file(nm_cfg) +- self.assertIn("ifcfg-rh", nm_file_content) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- +- def test_check_ifcfg_rh_plugins_no_plugins(self): +- """enable_ifcfg_plugin creates plugins value if missing.""" +- render_dir = self.tmp_path("render") +- os.makedirs(render_dir) +- nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file) +- util.ensure_dir(os.path.dirname(nm_cfg)) +- +- # write a template nm.conf, note plugins is missing +- util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\n") +- self.assertTrue(os.path.exists(nm_cfg)) +- +- # render and read +- entry = NETWORK_CONFIGS["small"] +- found = self._render_and_read( +- network_config=yaml.load(entry["yaml"]), dir=render_dir +- ) +- self._compare_files_to_expected(entry[self.expected_name], found) +- self._assert_headers(found) +- +- # check ifcfg-rh is in the 'plugins' list +- config = sysconfig.ConfigObj(nm_cfg) +- self.assertIn("ifcfg-rh", config["main"]["plugins"]) +- + def test_netplan_dhcp_false_disable_dhcp_in_state(self): + """netplan config with dhcp[46]: False should not add dhcp in state""" + net_config = yaml.load(NETPLAN_DHCP_FALSE) +@@ -6164,60 +6091,50 @@ def test_dhcpv6_reject_ra_config_v2(self, m_chown): + + class TestRenderersSelect: + @pytest.mark.parametrize( +- "renderer_selected,netplan,eni,nm,scfg,sys,networkd", ++ "renderer_selected,netplan,eni,sys,networkd", + ( +- # -netplan -ifupdown -nm -scfg -sys raises error ++ # -netplan -ifupdown -sys raises error + ( + net.RendererNotFoundError, + False, + False, + False, + False, +- False, +- False, + ), +- # -netplan +ifupdown -nm -scfg -sys selects eni +- ("eni", False, True, False, False, False, False), +- # +netplan +ifupdown -nm -scfg -sys selects eni +- ("eni", True, True, False, False, False, False), +- # +netplan -ifupdown -nm -scfg -sys selects netplan +- ("netplan", True, False, False, False, False, False), ++ # -netplan +ifupdown -sys selects eni ++ ("eni", False, True, False, False), ++ # +netplan +ifupdown -sys selects eni ++ ("eni", True, True, False, False), ++ # +netplan -ifupdown -sys selects netplan ++ ("netplan", True, False, False, False), + # Ubuntu with Network-Manager installed +- # +netplan -ifupdown +nm -scfg -sys selects netplan +- ("netplan", True, False, True, False, False, False), ++ # +netplan -ifupdown -sys selects netplan ++ ("netplan", True, False, False, False), + # Centos/OpenSuse with Network-Manager installed selects sysconfig +- # -netplan -ifupdown +nm -scfg +sys selects netplan +- ("sysconfig", False, False, True, False, True, False), +- # -netplan -ifupdown -nm -scfg -sys +networkd selects networkd +- ("networkd", False, False, False, False, False, True), ++ # -netplan -ifupdown +sys selects netplan ++ ("sysconfig", False, False, True, False), ++ # -netplan -ifupdown -sys +networkd selects networkd ++ ("networkd", False, False, False, True), + ), + ) + @mock.patch("cloudinit.net.renderers.networkd.available") + @mock.patch("cloudinit.net.renderers.netplan.available") + @mock.patch("cloudinit.net.renderers.sysconfig.available") +- @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig") +- @mock.patch("cloudinit.net.renderers.sysconfig.available_nm") + @mock.patch("cloudinit.net.renderers.eni.available") + def test_valid_renderer_from_defaults_depending_on_availability( + self, + m_eni_avail, +- m_nm_avail, +- m_scfg_avail, + m_sys_avail, + m_netplan_avail, + m_networkd_avail, + renderer_selected, + netplan, + eni, +- nm, +- scfg, + sys, + networkd, + ): + """Assert proper renderer per DEFAULT_PRIORITY given availability.""" + m_eni_avail.return_value = eni # ifupdown pkg presence +- m_nm_avail.return_value = nm # network-manager presence +- m_scfg_avail.return_value = scfg # sysconfig presence + m_sys_avail.return_value = sys # sysconfig/ifup/down presence + m_netplan_avail.return_value = netplan # netplan presence + m_networkd_avail.return_value = networkd # networkd presence +@@ -6277,7 +6194,7 @@ def test_select_none_found_raises(self, m_eni_avail, m_sysc_avail): + priority=["sysconfig", "eni"], + ) + +- @mock.patch("cloudinit.net.sysconfig.available_sysconfig") ++ @mock.patch("cloudinit.net.sysconfig.available") + @mock.patch("cloudinit.util.system_info") + def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail): + m_avail.return_value = True + +From c47045d47464d73004dac2a4938aaddda7f25f19 Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Mon, 31 Jan 2022 09:17:24 +0100 +Subject: [PATCH 4/6] Fix a couple of comments + +The comments above +test_valid_renderer_from_defaults_depending_on_availability() +were not quite right. +--- + tests/unittests/test_net.py | 14 ++++++-------- + 1 file changed, 6 insertions(+), 8 deletions(-) + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index c473c79142..b409c13c93 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -6093,7 +6093,7 @@ class TestRenderersSelect: + @pytest.mark.parametrize( + "renderer_selected,netplan,eni,sys,networkd", + ( +- # -netplan -ifupdown -sys raises error ++ # -netplan -ifupdown -sys -networkd raises error + ( + net.RendererNotFoundError, + False, +@@ -6101,17 +6101,15 @@ class TestRenderersSelect: + False, + False, + ), +- # -netplan +ifupdown -sys selects eni ++ # -netplan +ifupdown -sys -networkd selects eni + ("eni", False, True, False, False), +- # +netplan +ifupdown -sys selects eni ++ # +netplan +ifupdown -sys -networkd selects eni + ("eni", True, True, False, False), +- # +netplan -ifupdown -sys selects netplan ++ # +netplan -ifupdown -sys -networkd selects netplan + ("netplan", True, False, False, False), +- # Ubuntu with Network-Manager installed +- # +netplan -ifupdown -sys selects netplan ++ # +netplan -ifupdown -sys -networkd selects netplan + ("netplan", True, False, False, False), +- # Centos/OpenSuse with Network-Manager installed selects sysconfig +- # -netplan -ifupdown +sys selects netplan ++ # -netplan -ifupdown +sys -networkd selects sysconfig + ("sysconfig", False, False, True, False), + # -netplan -ifupdown -sys +networkd selects networkd + ("networkd", False, False, False, True), + +From 0cec899e804eb1a609f09d355384a3f41bf13575 Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Fri, 28 Jan 2022 12:29:25 +0100 +Subject: [PATCH 5/6] Add NetworkManager renderer + +This generates native NetworkManager keyfiles as opposed to relying on +ifcfg compatibility, because the later has been long deprecated and is +going to go away from new Fedora installations. +--- + cloudinit/cmd/devel/net_convert.py | 14 +- + cloudinit/net/activators.py | 25 +- + cloudinit/net/network_manager.py | 377 +++++++++++++++++++++++++ + cloudinit/net/renderers.py | 3 + + tests/unittests/test_net_activators.py | 93 +++++- + 5 files changed, 484 insertions(+), 28 deletions(-) + create mode 100644 cloudinit/net/network_manager.py + +diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py +index 18b1e7ff78..647fe07b09 100755 +--- a/cloudinit/cmd/devel/net_convert.py ++++ b/cloudinit/cmd/devel/net_convert.py +@@ -7,7 +7,14 @@ + import sys + + from cloudinit import distros, log, safeyaml +-from cloudinit.net import eni, netplan, network_state, networkd, sysconfig ++from cloudinit.net import ( ++ eni, ++ netplan, ++ network_manager, ++ network_state, ++ networkd, ++ sysconfig, ++) + from cloudinit.sources import DataSourceAzure as azure + from cloudinit.sources import DataSourceOVF as ovf + from cloudinit.sources.helpers import openstack +@@ -74,7 +81,7 @@ def get_parser(parser=None): + parser.add_argument( + "-O", + "--output-kind", +- choices=["eni", "netplan", "networkd", "sysconfig"], ++ choices=["eni", "netplan", "networkd", "sysconfig", "network-manager"], + required=True, + help="The network config format to emit", + ) +@@ -148,6 +155,9 @@ def handle_args(name, args): + elif args.output_kind == "sysconfig": + r_cls = sysconfig.Renderer + config = distro.renderer_configs.get("sysconfig") ++ elif args.output_kind == "network-manager": ++ r_cls = network_manager.Renderer ++ config = distro.renderer_configs.get("network-manager") + else: + raise RuntimeError("Invalid output_kind") + +diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py +index e80c26df38..edbc0c065b 100644 +--- a/cloudinit/net/activators.py ++++ b/cloudinit/net/activators.py +@@ -1,15 +1,14 @@ + # This file is part of cloud-init. See LICENSE file for license information. + import logging +-import os + from abc import ABC, abstractmethod + from typing import Iterable, List, Type + + from cloudinit import subp, util + from cloudinit.net.eni import available as eni_available + from cloudinit.net.netplan import available as netplan_available ++from cloudinit.net.network_manager import available as nm_available + from cloudinit.net.network_state import NetworkState + from cloudinit.net.networkd import available as networkd_available +-from cloudinit.net.sysconfig import NM_CFG_FILE + + LOG = logging.getLogger(__name__) + +@@ -124,20 +123,24 @@ def bring_down_interface(device_name: str) -> bool: + class NetworkManagerActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: +- """Return true if network manager can be used on this system.""" +- config_present = os.path.isfile( +- subp.target_path(target, path=NM_CFG_FILE) +- ) +- nmcli_present = subp.which("nmcli", target=target) +- return config_present and bool(nmcli_present) ++ """Return true if NetworkManager can be used on this system.""" ++ return nm_available(target=target) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: +- """Bring up interface using nmcli. ++ """Bring up connection using nmcli. + + Return True is successful, otherwise return False + """ +- cmd = ["nmcli", "connection", "up", "ifname", device_name] ++ from cloudinit.net.network_manager import conn_filename ++ ++ filename = conn_filename(device_name) ++ cmd = ["nmcli", "connection", "load", filename] ++ if _alter_interface(cmd, device_name): ++ cmd = ["nmcli", "connection", "up", "filename", filename] ++ else: ++ _alter_interface(["nmcli", "connection", "reload"], device_name) ++ cmd = ["nmcli", "connection", "up", "ifname", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod +@@ -146,7 +149,7 @@ def bring_down_interface(device_name: str) -> bool: + + Return True is successful, otherwise return False + """ +- cmd = ["nmcli", "connection", "down", device_name] ++ cmd = ["nmcli", "device", "disconnect", device_name] + return _alter_interface(cmd, device_name) + + +diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py +new file mode 100644 +index 0000000000..79b0fe0bf6 +--- /dev/null ++++ b/cloudinit/net/network_manager.py +@@ -0,0 +1,377 @@ ++# Copyright 2022 Red Hat, Inc. ++# ++# Author: Lubomir Rintel ++# Fixes and suggestions contributed by James Falcon, Neal Gompa, ++# Zbigniew Jędrzejewski-Szmek and Emanuele Giuseppe Esposito. ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++import configparser ++import io ++import itertools ++import os ++import uuid ++ ++from cloudinit import log as logging ++from cloudinit import subp, util ++ ++from . import renderer ++from .network_state import is_ipv6_addr, subnet_is_ipv6 ++ ++NM_RUN_DIR = "/etc/NetworkManager" ++NM_LIB_DIR = "/usr/lib/NetworkManager" ++LOG = logging.getLogger(__name__) ++ ++ ++class NMConnection: ++ """Represents a NetworkManager connection profile.""" ++ ++ def __init__(self, con_id): ++ """ ++ Initializes the connection with some very basic properties, ++ notably the UUID so that the connection can be referred to. ++ """ ++ ++ # Chosen by fair dice roll ++ CI_NM_UUID = uuid.UUID("a3924cb8-09e0-43e9-890b-77972a800108") ++ ++ self.config = configparser.ConfigParser() ++ # Identity option name mapping, to achieve case sensitivity ++ self.config.optionxform = str ++ ++ self.config["connection"] = { ++ "id": f"cloud-init {con_id}", ++ "uuid": str(uuid.uuid5(CI_NM_UUID, con_id)), ++ } ++ ++ # This is not actually used anywhere, but may be useful in future ++ self.config["user"] = { ++ "org.freedesktop.NetworkManager.origin": "cloud-init" ++ } ++ ++ def _set_default(self, section, option, value): ++ """ ++ Sets a property unless it's already set, ensuring the section ++ exists. ++ """ ++ ++ if not self.config.has_section(section): ++ self.config[section] = {} ++ if not self.config.has_option(section, option): ++ self.config[section][option] = value ++ ++ def _set_ip_method(self, family, subnet_type): ++ """ ++ Ensures there's appropriate [ipv4]/[ipv6] for given family ++ appropriate for given configuration type ++ """ ++ ++ method_map = { ++ "static": "manual", ++ "dhcp6": "dhcp", ++ "ipv6_slaac": "auto", ++ "ipv6_dhcpv6-stateless": "auto", ++ "ipv6_dhcpv6-stateful": "auto", ++ "dhcp4": "auto", ++ "dhcp": "auto", ++ } ++ ++ # Ensure we got an [ipvX] section ++ self._set_default(family, "method", "disabled") ++ ++ try: ++ method = method_map[subnet_type] ++ except KeyError: ++ # What else can we do ++ method = "auto" ++ self.config[family]["may-fail"] = "true" ++ ++ # Make sure we don't "downgrade" the method in case ++ # we got conflicting subnets (e.g. static along with dhcp) ++ if self.config[family]["method"] == "dhcp": ++ return ++ if self.config[family]["method"] == "auto" and method == "manual": ++ return ++ ++ self.config[family]["method"] = method ++ self._set_default(family, "may-fail", "false") ++ if family == "ipv6": ++ self._set_default(family, "addr-gen-mode", "stable-privacy") ++ ++ def _add_numbered(self, section, key_prefix, value): ++ """ ++ Adds a numbered property, such as address or route, ensuring ++ the appropriate value gets used for . ++ """ ++ ++ for index in itertools.count(1): ++ key = f"{key_prefix}{index}" ++ if not self.config.has_option(section, key): ++ self.config[section][key] = value ++ break ++ ++ def _add_address(self, family, subnet): ++ """ ++ Adds an ipv[46]address property. ++ """ ++ ++ value = subnet["address"] + "/" + str(subnet["prefix"]) ++ self._add_numbered(family, "address", value) ++ ++ def _add_route(self, family, route): ++ """ ++ Adds a ipv[46].route property. ++ """ ++ ++ value = route["network"] + "/" + str(route["prefix"]) ++ if "gateway" in route: ++ value = value + "," + route["gateway"] ++ self._add_numbered(family, "route", value) ++ ++ def _add_nameserver(self, dns): ++ """ ++ Extends the ipv[46].dns property with a name server. ++ """ ++ ++ # FIXME: the subnet contains IPv4 and IPv6 name server mixed ++ # together. We might be getting an IPv6 name server while ++ # we're dealing with an IPv4 subnet. Sort this out by figuring ++ # out the correct family and making sure a valid section exist. ++ family = "ipv6" if is_ipv6_addr(dns) else "ipv4" ++ self._set_default(family, "method", "disabled") ++ ++ self._set_default(family, "dns", "") ++ self.config[family]["dns"] = self.config[family]["dns"] + dns + ";" ++ ++ def _add_dns_search(self, family, dns_search): ++ """ ++ Extends the ipv[46].dns-search property with a name server. ++ """ ++ ++ self._set_default(family, "dns-search", "") ++ self.config[family]["dns-search"] = ( ++ self.config[family]["dns-search"] + ";".join(dns_search) + ";" ++ ) ++ ++ def con_uuid(self): ++ """ ++ Returns the connection UUID ++ """ ++ return self.config["connection"]["uuid"] ++ ++ def valid(self): ++ """ ++ Can this be serialized into a meaningful connection profile? ++ """ ++ return self.config.has_option("connection", "type") ++ ++ @staticmethod ++ def mac_addr(addr): ++ """ ++ Sanitize a MAC address. ++ """ ++ return addr.replace("-", ":").upper() ++ ++ def render_interface(self, iface, renderer): ++ """ ++ Integrate information from network state interface information ++ into the connection. Most of the work is done here. ++ """ ++ ++ # Initialize type & connectivity ++ _type_map = { ++ "physical": "ethernet", ++ "vlan": "vlan", ++ "bond": "bond", ++ "bridge": "bridge", ++ "infiniband": "infiniband", ++ "loopback": None, ++ } ++ ++ if_type = _type_map[iface["type"]] ++ if if_type is None: ++ return ++ if "bond-master" in iface: ++ slave_type = "bond" ++ else: ++ slave_type = None ++ ++ self.config["connection"]["type"] = if_type ++ if slave_type is not None: ++ self.config["connection"]["slave-type"] = slave_type ++ self.config["connection"]["master"] = renderer.con_ref( ++ iface[slave_type + "-master"] ++ ) ++ ++ # Add type specific-section ++ self.config[if_type] = {} ++ ++ # These are the interface properties that map nicely ++ # to NetworkManager properties ++ _prop_map = { ++ "bond": { ++ "mode": "bond-mode", ++ "miimon": "bond_miimon", ++ "xmit_hash_policy": "bond-xmit-hash-policy", ++ "num_grat_arp": "bond-num-grat-arp", ++ "downdelay": "bond-downdelay", ++ "updelay": "bond-updelay", ++ "fail_over_mac": "bond-fail-over-mac", ++ "primary_reselect": "bond-primary-reselect", ++ "primary": "bond-primary", ++ }, ++ "bridge": { ++ "stp": "bridge_stp", ++ "priority": "bridge_bridgeprio", ++ }, ++ "vlan": { ++ "id": "vlan_id", ++ }, ++ "ethernet": {}, ++ "infiniband": {}, ++ } ++ ++ device_mtu = iface["mtu"] ++ ipv4_mtu = None ++ ++ # Deal with Layer 3 configuration ++ for subnet in iface["subnets"]: ++ family = "ipv6" if subnet_is_ipv6(subnet) else "ipv4" ++ ++ self._set_ip_method(family, subnet["type"]) ++ if "address" in subnet: ++ self._add_address(family, subnet) ++ if "gateway" in subnet: ++ self.config[family]["gateway"] = subnet["gateway"] ++ for route in subnet["routes"]: ++ self._add_route(family, route) ++ if "dns_nameservers" in subnet: ++ for nameserver in subnet["dns_nameservers"]: ++ self._add_nameserver(nameserver) ++ if "dns_search" in subnet: ++ self._add_dns_search(family, subnet["dns_search"]) ++ if family == "ipv4" and "mtu" in subnet: ++ ipv4_mtu = subnet["mtu"] ++ ++ if ipv4_mtu is None: ++ ipv4_mtu = device_mtu ++ if not ipv4_mtu == device_mtu: ++ LOG.warning( ++ "Network config: ignoring %s device-level mtu:%s" ++ " because ipv4 subnet-level mtu:%s provided.", ++ iface["name"], ++ device_mtu, ++ ipv4_mtu, ++ ) ++ ++ # Parse type-specific properties ++ for nm_prop, key in _prop_map[if_type].items(): ++ if key not in iface: ++ continue ++ if iface[key] is None: ++ continue ++ if isinstance(iface[key], bool): ++ self.config[if_type][nm_prop] = ( ++ "true" if iface[key] else "false" ++ ) ++ else: ++ self.config[if_type][nm_prop] = str(iface[key]) ++ ++ # These ones need special treatment ++ if if_type == "ethernet": ++ if iface["wakeonlan"] is True: ++ # NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC ++ self.config["ethernet"]["wake-on-lan"] = str(0x40) ++ if ipv4_mtu is not None: ++ self.config["ethernet"]["mtu"] = str(ipv4_mtu) ++ if iface["mac_address"] is not None: ++ self.config["ethernet"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ if if_type == "vlan" and "vlan-raw-device" in iface: ++ self.config["vlan"]["parent"] = renderer.con_ref( ++ iface["vlan-raw-device"] ++ ) ++ if if_type == "bridge": ++ # Bridge is ass-backwards compared to bond ++ for port in iface["bridge_ports"]: ++ port = renderer.get_conn(port) ++ port._set_default("connection", "slave-type", "bridge") ++ port._set_default("connection", "master", self.con_uuid()) ++ if iface["mac_address"] is not None: ++ self.config["bridge"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ if if_type == "infiniband" and ipv4_mtu is not None: ++ self.config["infiniband"]["transport-mode"] = "datagram" ++ self.config["infiniband"]["mtu"] = str(ipv4_mtu) ++ if iface["mac_address"] is not None: ++ self.config["infiniband"]["mac-address"] = self.mac_addr( ++ iface["mac_address"] ++ ) ++ ++ # Finish up ++ if if_type == "bridge" or not self.config.has_option( ++ if_type, "mac-address" ++ ): ++ self.config["connection"]["interface-name"] = iface["name"] ++ ++ def dump(self): ++ """ ++ Stringify. ++ """ ++ ++ buf = io.StringIO() ++ self.config.write(buf, space_around_delimiters=False) ++ header = "# Generated by cloud-init. Changes will be lost.\n\n" ++ return header + buf.getvalue() ++ ++ ++class Renderer(renderer.Renderer): ++ """Renders network information in a NetworkManager keyfile format.""" ++ ++ def __init__(self, config=None): ++ self.connections = {} ++ ++ def get_conn(self, con_id): ++ return self.connections[con_id] ++ ++ def con_ref(self, con_id): ++ if con_id in self.connections: ++ return self.connections[con_id].con_uuid() ++ else: ++ # Well, what can we do... ++ return con_id ++ ++ def render_network_state(self, network_state, templates=None, target=None): ++ # First pass makes sure there's NMConnections for all known ++ # interfaces that have UUIDs that can be linked to from related ++ # interfaces ++ for iface in network_state.iter_interfaces(): ++ self.connections[iface["name"]] = NMConnection(iface["name"]) ++ ++ # Now render the actual interface configuration ++ for iface in network_state.iter_interfaces(): ++ conn = self.connections[iface["name"]] ++ conn.render_interface(iface, self) ++ ++ # And finally write the files ++ for con_id, conn in self.connections.items(): ++ if not conn.valid(): ++ continue ++ name = conn_filename(con_id, target) ++ util.write_file(name, conn.dump(), 0o600) ++ ++ ++def conn_filename(con_id, target=None): ++ target_con_dir = subp.target_path(target, NM_RUN_DIR) ++ con_file = f"cloud-init-{con_id}.nmconnection" ++ return f"{target_con_dir}/system-connections/{con_file}" ++ ++ ++def available(target=None): ++ target_nm_dir = subp.target_path(target, NM_LIB_DIR) ++ return os.path.exists(target_nm_dir) ++ ++ ++# vi: ts=4 expandtab +diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py +index c755f04c1c..7edc34b55a 100644 +--- a/cloudinit/net/renderers.py ++++ b/cloudinit/net/renderers.py +@@ -8,6 +8,7 @@ + freebsd, + netbsd, + netplan, ++ network_manager, + networkd, + openbsd, + renderer, +@@ -19,6 +20,7 @@ + "freebsd": freebsd, + "netbsd": netbsd, + "netplan": netplan, ++ "network-manager": network_manager, + "networkd": networkd, + "openbsd": openbsd, + "sysconfig": sysconfig, +@@ -28,6 +30,7 @@ + "eni", + "sysconfig", + "netplan", ++ "network-manager", + "freebsd", + "netbsd", + "openbsd", +diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py +index 3c29e2f752..4525c49c1b 100644 +--- a/tests/unittests/test_net_activators.py ++++ b/tests/unittests/test_net_activators.py +@@ -41,18 +41,20 @@ + + @pytest.fixture + def available_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file") ++ mocks = namedtuple("Mocks", "m_which, m_file, m_exists") + with patch("cloudinit.subp.which", return_value=True) as m_which: + with patch("os.path.isfile", return_value=True) as m_file: +- yield mocks(m_which, m_file) ++ with patch("os.path.exists", return_value=True) as m_exists: ++ yield mocks(m_which, m_file, m_exists) + + + @pytest.fixture + def unavailable_mocks(): +- mocks = namedtuple("Mocks", "m_which, m_file") ++ mocks = namedtuple("Mocks", "m_which, m_file, m_exists") + with patch("cloudinit.subp.which", return_value=False) as m_which: + with patch("os.path.isfile", return_value=False) as m_file: +- yield mocks(m_which, m_file) ++ with patch("os.path.exists", return_value=False) as m_exists: ++ yield mocks(m_which, m_file, m_exists) + + + class TestSearchAndSelect: +@@ -113,10 +115,6 @@ def test_none_available(self, unavailable_mocks): + (("netplan",), {"search": ["/usr/sbin", "/sbin"], "target": None}), + ] + +-NETWORK_MANAGER_AVAILABLE_CALLS = [ +- (("nmcli",), {"target": None}), +-] +- + NETWORKD_AVAILABLE_CALLS = [ + (("ip",), {"search": ["/usr/sbin", "/bin"], "target": None}), + (("systemctl",), {"search": ["/usr/sbin", "/bin"], "target": None}), +@@ -128,7 +126,6 @@ def test_none_available(self, unavailable_mocks): + [ + (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS), + (NetplanActivator, NETPLAN_AVAILABLE_CALLS), +- (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS), + (NetworkdActivator, NETWORKD_AVAILABLE_CALLS), + ], + ) +@@ -144,8 +141,72 @@ def test_available(self, activator, available_calls, available_mocks): + ] + + NETWORK_MANAGER_BRING_UP_CALL_LIST = [ +- ((["nmcli", "connection", "up", "ifname", "eth0"],), {}), +- ((["nmcli", "connection", "up", "ifname", "eth1"],), {}), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "load", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "up", ++ "filename", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "load", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth1.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), ++ ( ++ ( ++ [ ++ "nmcli", ++ "connection", ++ "up", ++ "filename", ++ "".join( ++ [ ++ "/etc/NetworkManager/system-connections", ++ "/cloud-init-eth1.nmconnection", ++ ] ++ ), ++ ], ++ ), ++ {}, ++ ), + ] + + NETWORKD_BRING_UP_CALL_LIST = [ +@@ -169,9 +230,11 @@ class TestActivatorsBringUp: + def test_bring_up_interface( + self, m_subp, activator, expected_call_list, available_mocks + ): ++ index = 0 + activator.bring_up_interface("eth0") +- assert len(m_subp.call_args_list) == 1 +- assert m_subp.call_args_list[0] == expected_call_list[0] ++ for call in m_subp.call_args_list: ++ assert call == expected_call_list[index] ++ index += 1 + + @patch("cloudinit.subp.subp", return_value=("", "")) + def test_bring_up_interfaces( +@@ -208,8 +271,8 @@ def test_bring_up_all_interfaces_v2( + ] + + NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [ +- ((["nmcli", "connection", "down", "eth0"],), {}), +- ((["nmcli", "connection", "down", "eth1"],), {}), ++ ((["nmcli", "device", "disconnect", "eth0"],), {}), ++ ((["nmcli", "device", "disconnect", "eth1"],), {}), + ] + + NETWORKD_BRING_DOWN_CALL_LIST = [ + +From 07e503391d4750a938fedb7ec9240b12af2b0896 Mon Sep 17 00:00:00 2001 +From: Lubomir Rintel +Date: Fri, 28 Jan 2022 17:36:43 +0100 +Subject: [PATCH 6/6] Add unit tests for the NetworkManager network renderer + +The test fixture is based upon what NetworkManager would generate when +reading in the legacy ifcfg (sysconfig) files. +--- + tests/unittests/test_net.py | 1121 ++++++++++++++++++++++++++++++++++- + 1 file changed, 1107 insertions(+), 14 deletions(-) + +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index b409c13c93..9552ac12f7 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -21,6 +21,7 @@ + interface_has_own_mac, + natural_sort_key, + netplan, ++ network_manager, + network_state, + networkd, + renderers, +@@ -612,6 +613,37 @@ + ), + ), + ], ++ "expected_network_manager": [ ++ ( ++ "".join( ++ [ ++ "etc/NetworkManager/system-connections", ++ "/cloud-init-eth0.nmconnection", ++ ] ++ ), ++ """ ++# Generated by cloud-init. Changes will be lost. ++ ++[connection] ++id=cloud-init eth0 ++uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++type=ethernet ++ ++[user] ++org.freedesktop.NetworkManager.origin=cloud-init ++ ++[ethernet] ++mac-address=FA:16:3E:ED:9A:59 ++ ++[ipv4] ++method=manual ++may-fail=false ++address1=172.19.1.34/22 ++route1=0.0.0.0/0,172.19.3.254 ++ ++""".lstrip(), ++ ), ++ ], + }, + { + "in_data": { +@@ -1078,6 +1110,50 @@ + USERCTL=no""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=CF:D6:AF:48:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth99.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth99 ++ uuid=b1b88000-1f03-5360-8377-1a2205efffb4 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:D6:9F:2C:E8:80 ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ address1=192.168.21.3/24 ++ route1=0.0.0.0/0,65.61.151.37 ++ dns=8.8.8.8;8.8.4.4; ++ dns-search=barley.maas;sach.maas; ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -1150,6 +1226,34 @@ + STARTMODE=auto""" + ) + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1253,6 +1357,37 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mtu=9000 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.14.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/64 ++ ++ """ ++ ), ++ }, + }, + "v6_and_v4": { + "expected_sysconfig_opensuse": { +@@ -1263,6 +1398,34 @@ + STARTMODE=auto""" + ) + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """\ + version: 1 +@@ -1336,6 +1499,30 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "dhcpv6_accept_ra": { + "expected_eni": textwrap.dedent( +@@ -1543,6 +1730,30 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=auto ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "static6": { + "yaml": textwrap.dedent( +@@ -1631,6 +1842,30 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv6] ++ method=auto ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ }, + }, + "dhcpv6_stateful": { + "expected_eni": textwrap.dedent( +@@ -1730,6 +1965,29 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -1783,6 +2041,30 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-iface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init iface0 ++ uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70 ++ type=ethernet ++ interface-name=iface0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ wake-on-lan=64 ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, + "yaml_v2": textwrap.dedent( + """\ + version: 2 +@@ -2231,6 +2513,254 @@ + USERCTL=no""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-eth3.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth3 ++ uuid=b7e95dda-7746-5bf8-bf33-6e5f3c926790 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=66:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth5.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth5 ++ uuid=5fda13c7-9942-5e90-a41b-1d043bd725dc ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=98:BB:9F:2C:E8:8A ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ "cloud-init-ib0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init ib0 ++ uuid=11a1dda7-78b4-5529-beba-d9b5f549ad7b ++ type=infiniband ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [infiniband] ++ transport-mode=datagram ++ mtu=9000 ++ mac-address=A0:00:02:20:FE:80:00:00:00:00:00:00:EC:0D:9A:03:00:15:E2:C1 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.200.7/24 ++ ++ """ ++ ), ++ "cloud-init-bond0.200.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0.200 ++ uuid=88984a9c-ff22-5233-9267-86315e0acaa7 ++ type=vlan ++ interface-name=bond0.200 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=200 ++ parent=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:D6:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth4.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth4 ++ uuid=e27e4959-fb50-5580-b9a4-2073554627b9 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=98:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:D6:9F:2C:E8:80 ++ ++ """ ++ ), ++ "cloud-init-br0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init br0 ++ uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ type=bridge ++ interface-name=br0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bridge] ++ stp=false ++ priority=22 ++ mac-address=BB:BB:BB:BB:BB:AA ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.14.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/64 ++ route1=::/0,2001:4800:78ff:1b::1 ++ ++ """ ++ ), ++ "cloud-init-eth0.101.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0.101 ++ uuid=b5acec5e-db80-5935-8b02-0d5619fc42bf ++ type=vlan ++ interface-name=eth0.101 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=101 ++ parent=1dd9a779-d327-56e1-8454-c65e2556c12c ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.0.2/24 ++ gateway=192.168.0.1 ++ dns=192.168.0.10;10.23.23.134; ++ dns-search=barley.maas;sacchromyces.maas;brettanomyces.maas; ++ address2=192.168.2.10/24 ++ ++ """ ++ ), ++ "cloud-init-bond0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0 ++ uuid=54317911-f840-516b-a10d-82cb4c1f075c ++ type=bond ++ interface-name=bond0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bond] ++ mode=active-backup ++ miimon=100 ++ xmit_hash_policy=layer3+4 ++ ++ [ipv6] ++ method=dhcp ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ ++ """ ++ ), ++ "cloud-init-eth2.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth2 ++ uuid=5559a242-3421-5fdd-896e-9cb8313d5804 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=C0:BB:9F:2C:E8:80 ++ ++ """ ++ ), ++ }, + "yaml": textwrap.dedent( + """ + version: 1 +@@ -2737,6 +3267,88 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-bond0s0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0s0 ++ uuid=09d0b5b9-67e7-5577-a1af-74d1cf17a71e ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:00 ++ ++ """ ++ ), ++ "cloud-init-bond0s1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0s1 ++ uuid=4d9aca96-b515-5630-ad83-d13daac7f9d0 ++ type=ethernet ++ slave-type=bond ++ master=54317911-f840-516b-a10d-82cb4c1f075c ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:01 ++ ++ """ ++ ), ++ "cloud-init-bond0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init bond0 ++ uuid=54317911-f840-516b-a10d-82cb4c1f075c ++ type=bond ++ interface-name=bond0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bond] ++ mode=active-backup ++ miimon=100 ++ xmit_hash_policy=layer3+4 ++ num_grat_arp=5 ++ downdelay=10 ++ updelay=20 ++ fail_over_mac=active ++ primary_reselect=always ++ primary=bond0s0 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.0.2/24 ++ gateway=192.168.0.1 ++ route1=10.1.3.0/24,192.168.0.3 ++ address2=192.168.1.2/24 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::1/92 ++ route1=2001:67c:1/32,2001:67c:1562::1 ++ route2=3001:67c:1/32,3001:67c:15::1 ++ ++ """ ++ ), ++ }, + }, + "vlan": { + "yaml": textwrap.dedent( +@@ -2822,6 +3434,58 @@ + VLAN=yes""" + ), + }, ++ "expected_network_manager": { ++ "cloud-init-en0.99.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init en0.99 ++ uuid=f594e2ed-f107-51df-b225-1dc530a5356b ++ type=vlan ++ interface-name=en0.99 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [vlan] ++ id=99 ++ parent=e0ca478b-8d84-52ab-8fae-628482c629b5 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.2.2/24 ++ address2=192.168.1.2/24 ++ gateway=192.168.1.1 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::bbbb/96 ++ route1=::/0,2001:1::1 ++ ++ """ ++ ), ++ "cloud-init-en0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init en0 ++ uuid=e0ca478b-8d84-52ab-8fae-628482c629b5 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=AA:BB:CC:DD:E8:00 ++ ++ """ ++ ), ++ }, + }, + "bridge": { + "yaml": textwrap.dedent( +@@ -2931,6 +3595,82 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-br0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init br0 ++ uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ type=bridge ++ interface-name=br0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [bridge] ++ stp=false ++ priority=22 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.2.2/24 ++ ++ """ ++ ), ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:00 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::100/96 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ slave-type=bridge ++ master=dee46ce4-af7a-5e7c-aa08-b25533ae9213 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:01 ++ ++ [ipv6] ++ method=manual ++ may-fail=false ++ addr-gen-mode=stable-privacy ++ address1=2001:1::101/96 ++ ++ """ ++ ), ++ }, + }, + "manual": { + "yaml": textwrap.dedent( +@@ -3062,6 +3802,73 @@ + """ + ), + }, ++ "expected_network_manager": { ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:00 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=192.168.1.2/24 ++ ++ """ ++ ), ++ "cloud-init-eth1.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1 ++ uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mtu=1480 ++ mac-address=52:54:00:12:34:AA ++ ++ [ipv4] ++ method=auto ++ may-fail=true ++ ++ """ ++ ), ++ "cloud-init-eth2.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth2 ++ uuid=5559a242-3421-5fdd-896e-9cb8313d5804 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:FF ++ ++ [ipv4] ++ method=auto ++ may-fail=true ++ ++ """ ++ ), ++ }, + }, + } + +@@ -4654,6 +5461,281 @@ def test_render_v6_and_v4(self): + self._assert_headers(found) + + ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False), ++) ++class TestNetworkManagerRendering(CiTestCase): ++ ++ with_logs = True ++ ++ scripts_dir = "/etc/NetworkManager/system-connections" ++ ++ expected_name = "expected_network_manager" ++ ++ def _get_renderer(self): ++ return network_manager.Renderer() ++ ++ 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 = self._get_renderer() ++ renderer.render_network_state(ns, target=dir) ++ return dir2dict(dir) ++ ++ def _compare_files_to_expected(self, expected, found): ++ orig_maxdiff = self.maxDiff ++ expected_d = dict( ++ (os.path.join(self.scripts_dir, k), v) for k, v in expected.items() ++ ) ++ ++ try: ++ self.maxDiff = None ++ self.assertEqual(expected_d, found) ++ finally: ++ self.maxDiff = orig_maxdiff ++ ++ @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") ++ @mock.patch("cloudinit.net.sys_dev_path") ++ @mock.patch("cloudinit.net.read_sys_net") ++ @mock.patch("cloudinit.net.get_devicelist") ++ def test_default_generation( ++ self, ++ mock_get_devicelist, ++ mock_read_sys_net, ++ mock_sys_dev_path, ++ m_get_cmdline, ++ ): ++ tmp_dir = self.tmp_dir() ++ _setup_test( ++ tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path ++ ) ++ ++ network_cfg = net.generate_fallback_config() ++ ns = network_state.parse_net_config_data( ++ network_cfg, skip_broken=False ++ ) ++ ++ render_dir = os.path.join(tmp_dir, "render") ++ os.makedirs(render_dir) ++ ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-eth1000.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth1000 ++ uuid=8c517500-0c95-5308-9c8a-3092eebc44eb ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=07:1C:C6:75:A4:BE ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_openstack_rendering_samples(self): ++ for os_sample in OS_SAMPLES: ++ render_dir = self.tmp_dir() ++ ex_input = os_sample["in_data"] ++ ex_mac_addrs = os_sample["in_macs"] ++ network_cfg = openstack.convert_net_json( ++ ex_input, known_macs=ex_mac_addrs ++ ) ++ ns = network_state.parse_net_config_data( ++ network_cfg, skip_broken=False ++ ) ++ renderer = self._get_renderer() ++ # render a multiple times to simulate reboots ++ renderer.render_network_state(ns, target=render_dir) ++ renderer.render_network_state(ns, target=render_dir) ++ renderer.render_network_state(ns, target=render_dir) ++ for fn, expected_content in os_sample.get(self.expected_name, []): ++ with open(os.path.join(render_dir, fn)) as fh: ++ self.assertEqual(expected_content, fh.read()) ++ ++ def test_network_config_v1_samples(self): ++ ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET) ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-interface0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init interface0 ++ uuid=8b6862ed-dbd6-5830-93f7-a91451c13828 ++ type=ethernet ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ mac-address=52:54:00:12:34:00 ++ ++ [ipv4] ++ method=manual ++ may-fail=false ++ address1=10.0.2.15/24 ++ gateway=10.0.2.2 ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_config_with_explicit_loopback(self): ++ render_dir = self.tmp_path("render") ++ os.makedirs(render_dir) ++ ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK) ++ renderer = self._get_renderer() ++ renderer.render_network_state(ns, target=render_dir) ++ found = dir2dict(render_dir) ++ self._compare_files_to_expected( ++ { ++ "cloud-init-eth0.nmconnection": textwrap.dedent( ++ """\ ++ # Generated by cloud-init. Changes will be lost. ++ ++ [connection] ++ id=cloud-init eth0 ++ uuid=1dd9a779-d327-56e1-8454-c65e2556c12c ++ type=ethernet ++ interface-name=eth0 ++ ++ [user] ++ org.freedesktop.NetworkManager.origin=cloud-init ++ ++ [ethernet] ++ ++ [ipv4] ++ method=auto ++ may-fail=false ++ ++ """ ++ ), ++ }, ++ found, ++ ) ++ ++ def test_bond_config(self): ++ entry = NETWORK_CONFIGS["bond"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_vlan_config(self): ++ entry = NETWORK_CONFIGS["vlan"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_bridge_config(self): ++ entry = NETWORK_CONFIGS["bridge"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_manual_config(self): ++ entry = NETWORK_CONFIGS["manual"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_all_config(self): ++ entry = NETWORK_CONFIGS["all"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ self.assertNotIn( ++ "WARNING: Network config: ignoring eth0.101 device-level mtu", ++ self.logs.getvalue(), ++ ) ++ ++ def test_small_config(self): ++ entry = NETWORK_CONFIGS["small"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_v4_and_v6_static_config(self): ++ entry = NETWORK_CONFIGS["v4_and_v6_static"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ expected_msg = ( ++ "WARNING: Network config: ignoring iface0 device-level mtu:8999" ++ " because ipv4 subnet-level mtu:9000 provided." ++ ) ++ self.assertIn(expected_msg, self.logs.getvalue()) ++ ++ def test_dhcpv6_only_config(self): ++ entry = NETWORK_CONFIGS["dhcpv6_only"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_simple_render_ipv6_slaac(self): ++ entry = NETWORK_CONFIGS["ipv6_slaac"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_dhcpv6_stateless_config(self): ++ entry = NETWORK_CONFIGS["dhcpv6_stateless"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_wakeonlan_disabled_config_v2(self): ++ entry = NETWORK_CONFIGS["wakeonlan_disabled"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml_v2"]) ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_wakeonlan_enabled_config_v2(self): ++ entry = NETWORK_CONFIGS["wakeonlan_enabled"] ++ found = self._render_and_read( ++ network_config=yaml.load(entry["yaml_v2"]) ++ ) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_render_v4_and_v6(self): ++ entry = NETWORK_CONFIGS["v4_and_v6"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ def test_render_v6_and_v4(self): ++ entry = NETWORK_CONFIGS["v6_and_v4"] ++ found = self._render_and_read(network_config=yaml.load(entry["yaml"])) ++ self._compare_files_to_expected(entry[self.expected_name], found) ++ ++ ++@mock.patch( ++ "cloudinit.net.is_openvswitch_internal_interface", ++ mock.Mock(return_value=False), ++) + class TestEniNetRendering(CiTestCase): + @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") + @mock.patch("cloudinit.net.sys_dev_path") +@@ -6091,31 +7173,39 @@ def test_dhcpv6_reject_ra_config_v2(self, m_chown): + + class TestRenderersSelect: + @pytest.mark.parametrize( +- "renderer_selected,netplan,eni,sys,networkd", ++ "renderer_selected,netplan,eni,sys,network_manager,networkd", + ( +- # -netplan -ifupdown -sys -networkd raises error ++ # -netplan -ifupdown -sys -network-manager -networkd raises error + ( + net.RendererNotFoundError, + False, + False, + False, + False, ++ False, + ), +- # -netplan +ifupdown -sys -networkd selects eni +- ("eni", False, True, False, False), +- # +netplan +ifupdown -sys -networkd selects eni +- ("eni", True, True, False, False), +- # +netplan -ifupdown -sys -networkd selects netplan +- ("netplan", True, False, False, False), +- # +netplan -ifupdown -sys -networkd selects netplan +- ("netplan", True, False, False, False), +- # -netplan -ifupdown +sys -networkd selects sysconfig +- ("sysconfig", False, False, True, False), +- # -netplan -ifupdown -sys +networkd selects networkd +- ("networkd", False, False, False, True), ++ # -netplan +ifupdown -sys -nm -networkd selects eni ++ ("eni", False, True, False, False, False), ++ # +netplan +ifupdown -sys -nm -networkd selects eni ++ ("eni", True, True, False, False, False), ++ # +netplan -ifupdown -sys -nm -networkd selects netplan ++ ("netplan", True, False, False, False, False), ++ # +netplan -ifupdown -sys -nm -networkd selects netplan ++ ("netplan", True, False, False, False, False), ++ # -netplan -ifupdown +sys -nm -networkd selects sysconfig ++ ("sysconfig", False, False, True, False, False), ++ # -netplan -ifupdown +sys +nm -networkd selects sysconfig ++ ("sysconfig", False, False, True, True, False), ++ # -netplan -ifupdown -sys +nm -networkd selects nm ++ ("network-manager", False, False, False, True, False), ++ # -netplan -ifupdown -sys +nm +networkd selects nm ++ ("network-manager", False, False, False, True, True), ++ # -netplan -ifupdown -sys -nm +networkd selects networkd ++ ("networkd", False, False, False, False, True), + ), + ) + @mock.patch("cloudinit.net.renderers.networkd.available") ++ @mock.patch("cloudinit.net.renderers.network_manager.available") + @mock.patch("cloudinit.net.renderers.netplan.available") + @mock.patch("cloudinit.net.renderers.sysconfig.available") + @mock.patch("cloudinit.net.renderers.eni.available") +@@ -6124,17 +7214,20 @@ def test_valid_renderer_from_defaults_depending_on_availability( + m_eni_avail, + m_sys_avail, + m_netplan_avail, ++ m_network_manager_avail, + m_networkd_avail, + renderer_selected, + netplan, + eni, + sys, ++ network_manager, + networkd, + ): + """Assert proper renderer per DEFAULT_PRIORITY given availability.""" + m_eni_avail.return_value = eni # ifupdown pkg presence + m_sys_avail.return_value = sys # sysconfig/ifup/down presence + m_netplan_avail.return_value = netplan # netplan presence ++ m_network_manager_avail.return_value = network_manager # NM presence + m_networkd_avail.return_value = networkd # networkd presence + if isinstance(renderer_selected, str): + (renderer_name, _rnd_class) = renderers.select( diff --git a/cloud-init-22.1-nm-default.patch b/cloud-init-22.1-nm-default.patch new file mode 100644 index 0000000..720fd58 --- /dev/null +++ b/cloud-init-22.1-nm-default.patch @@ -0,0 +1,48 @@ +From b813d8b59c46148dcbc7ff9f36e2aac7cce38373 Mon Sep 17 00:00:00 2001 +From: Neal Gompa +Date: Tue, 22 Feb 2022 07:20:17 -0500 +Subject: [PATCH] net: Prefer NetworkManager renderer by default + +NetworkManager is used by default on a variety of Linux distributions, +and exists as a cross-distribution network management service. + +Signed-off-by: Neal Gompa +--- + cloudinit/net/renderers.py | 2 +- + tests/unittests/test_net.py | 4 ++-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py +index 7edc34b5..d958c652 100644 +--- a/cloudinit/net/renderers.py ++++ b/cloudinit/net/renderers.py +@@ -27,10 +27,10 @@ NAME_TO_RENDERER = { + } + + DEFAULT_PRIORITY = [ ++ "network-manager", + "eni", + "sysconfig", + "netplan", +- "network-manager", + "freebsd", + "netbsd", + "openbsd", +diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 9552ac12..668f2c45 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -7194,8 +7194,8 @@ class TestRenderersSelect: + ("netplan", True, False, False, False, False), + # -netplan -ifupdown +sys -nm -networkd selects sysconfig + ("sysconfig", False, False, True, False, False), +- # -netplan -ifupdown +sys +nm -networkd selects sysconfig +- ("sysconfig", False, False, True, True, False), ++ # -netplan -ifupdown +sys +nm -networkd selects network-manager ++ ("network-manager", False, False, True, True, False), + # -netplan -ifupdown -sys +nm -networkd selects nm + ("network-manager", False, False, False, True, False), + # -netplan -ifupdown -sys +nm +networkd selects nm +-- +2.34.1 + diff --git a/cloud-init.spec b/cloud-init.spec index c13aea5..4808dc1 100644 --- a/cloud-init.spec +++ b/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init -Version: 21.3 -Release: 5%{?dist} +Version: 22.1 +Release: 1%{?dist} Summary: Cloud instance init scripts License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -8,27 +8,22 @@ URL: http://launchpad.net/cloud-init Source0: https://launchpad.net/cloud-init/trunk/%{version}/+download/%{name}-%{version}.tar.gz Source1: cloud-init-tmpfiles.conf -# Disable tests that require pylxd, which we don't have on Fedora -Patch1: cloud-init-21.3-disable-lxd-tests.patch +# Add full support for NetworkManager +# From: https://github.com/canonical/cloud-init/pull/1224 +# https://bugzilla.redhat.com/show_bug.cgi?id=2014701 +Patch1: cloud-init-22.1-PR1224-full-nm-support.patch -# Do not write NM_CONTROLLED=no in generated interface config files -# https://bugzilla.redhat.com/show_bug.cgi?id=1385172 -Patch2: cloud-init-21.3-nm-controlled.patch - -# Keep old properties in /etc/sysconfig/network -# https://bugzilla.redhat.com/show_bug.cgi?id=1558641 -Patch3: cloud-init-21.3-no-override-default-network.patch +# Default to NetworkManager for configuration renderer +# https://bugzilla.redhat.com/show_bug.cgi?id=2014701 +Patch2: cloud-init-22.1-nm-default.patch # Adding default RHEL configuration file -Patch4: cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch - -# Fix tests with latest pyyaml -# From: https://github.com/canonical/cloud-init/commit/125dcb28ea30affeec44029d99bee4b130d5fdc8 -Patch5: cloud-init-21.4-Fix-unit-test-broken-by-pyyaml-upgrade.patch +Patch3: cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch BuildArch: noarch BuildRequires: pkgconfig(systemd) +BuildRequires: systemd-rpm-macros BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: systemd @@ -57,6 +52,7 @@ BuildRequires: python3-netifaces # https://bugs.launchpad.net/cloud-init/+bug/1721573 BuildRequires: /usr/bin/dnf +Requires: (NetworkManager-config-server if NetworkManager) Requires: hostname Requires: e2fsprogs Requires: iproute @@ -104,8 +100,8 @@ sed -i -e 's|#!/usr/bin/python||' cloudinit/cmd/main.py # Use unittest from the standard library. unittest2 is old and being # retired in Fedora. See https://bugzilla.redhat.com/show_bug.cgi?id=1794222 -find cloudinit/tests/ tests/ -type f | xargs sed -i s/unittest2/unittest/ -find cloudinit/tests/ tests/ -type f | xargs sed -i s/assertItemsEqual/assertCountEqual/ +find tests/ -type f | xargs sed -i s/unittest2/unittest/ +find tests/ tests/ -type f | xargs sed -i s/assertItemsEqual/assertCountEqual/ %build %py3_build @@ -137,6 +133,10 @@ for man in cloud-id.1 cloud-init.1 cloud-init-per.1; do chmod -x ${RPM_BUILD_ROOT}%{_mandir}/man1/* done +# Put files in /etc/systemd/system in the right place +cp -a %{buildroot}/etc/systemd %{buildroot}/usr/lib +rm -rf %{buildroot}/etc/systemd + %check python3 -m pytest tests/unittests @@ -175,9 +175,9 @@ python3 -m pytest tests/unittests %{_unitdir}/cloud-config.target %{_unitdir}/cloud-init.target /usr/lib/systemd/system-generators/cloud-init-generator -/lib/udev/rules.d/10-cloud-init-hook-hotplug.rules -/usr/lib/systemd/system/cloud-init-hotplugd.service -/usr/lib/systemd/system/cloud-init-hotplugd.socket +%{_unitdir}/cloud-init-hotplugd.service +%{_unitdir}/cloud-init-hotplugd.socket +%{_unitdir}/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf %{_tmpfilesdir}/%{name}.conf %{python3_sitelib}/* %{_libexecdir}/%{name} @@ -189,6 +189,11 @@ python3 -m pytest tests/unittests %changelog +* Tue Feb 22 2022 Neal Gompa - 22.1-1 +- Rebase to 22.1 +- Backport cloud-init PR to add proper NetworkManager support [bz#2014701] +- Add patch to prefer NetworkManager [bz#2014701] + * Wed Jan 19 2022 Fedora Release Engineering - 21.3-5 - Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild diff --git a/sources b/sources index 5de7b26..e4f7158 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (cloud-init-21.3.tar.gz) = d248add04b3bf3807ca397fb075a20f55cff9a98116e349c5bf59b44e2d2e76559f92ac37d619551e2c871cdfb35c27a58f4759078d0355d6868e85d13c527bd +SHA512 (cloud-init-22.1.tar.gz) = 485e358777379a22dd2b0f6aa7afb1751eb44831c6e03ecbbd9c6823eaa20535e6e83fc245818ce1bb207425976839b356dadcfa3cfe62385b9d340b08ff21ab