From a32e90573fe22f4d133c40c664cc42eaf6e476c3 Mon Sep 17 00:00:00 2001 From: Neal Gompa Date: Thu, 19 May 2022 09:44:52 -0400 Subject: [PATCH] Rebase to 22.2 --- ...t-21.3-Adding-RHEL-default-cloud.cfg.patch | 88 - cloud-init-22.1-PR1224-full-nm-support.patch | 2442 ----------------- cloud-init-22.1-nm-default.patch | 48 - cloud-init-22.3-nm-default.patch | 94 + cloud-init.spec | 27 +- sources | 2 +- 6 files changed, 106 insertions(+), 2595 deletions(-) delete mode 100644 cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch delete mode 100644 cloud-init-22.1-PR1224-full-nm-support.patch delete mode 100644 cloud-init-22.1-nm-default.patch create mode 100644 cloud-init-22.3-nm-default.patch diff --git a/cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch b/cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch deleted file mode 100644 index 214f93a..0000000 --- a/cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch +++ /dev/null @@ -1,88 +0,0 @@ -From d46ac3af1e964916f65dd920fc54bd8c0c8e32de Mon Sep 17 00:00:00 2001 -From: Eduardo Otubo -Date: Thu, 10 Dec 2020 17:43:15 +0100 -Subject: [PATCH] Adding RHEL default cloud.cfg - ---- - rhel/cloud.cfg | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 69 insertions(+) - create mode 100644 rhel/cloud.cfg - -diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg -new file mode 100644 -index 00000000..9ecba215 ---- /dev/null -+++ b/rhel/cloud.cfg -@@ -0,0 +1,69 @@ -+users: -+ - default -+ -+disable_root: 1 -+ssh_pwauth: 0 -+ -+mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2'] -+resize_rootfs_tmp: /dev -+ssh_deletekeys: 1 -+ssh_genkeytypes: ~ -+syslog_fix_perms: ~ -+disable_vmware_customization: false -+ -+cloud_init_modules: -+ - disk_setup -+ - migrator -+ - bootcmd -+ - write-files -+ - growpart -+ - resizefs -+ - set_hostname -+ - update_hostname -+ - update_etc_hosts -+ - rsyslog -+ - users-groups -+ - ssh -+ -+cloud_config_modules: -+ - mounts -+ - locale -+ - set-passwords -+ - rh_subscription -+ - yum-add-repo -+ - package-update-upgrade-install -+ - timezone -+ - puppet -+ - chef -+ - salt-minion -+ - mcollective -+ - disable-ec2-metadata -+ - runcmd -+ -+cloud_final_modules: -+ - rightscale_userdata -+ - scripts-per-once -+ - scripts-per-boot -+ - scripts-per-instance -+ - scripts-user -+ - ssh-authkey-fingerprints -+ - keys-to-console -+ - phone-home -+ - final-message -+ - power-state-change -+ -+system_info: -+ default_user: -+ name: cloud-user -+ lock_passwd: true -+ gecos: Cloud User -+ groups: [adm, systemd-journal] -+ sudo: ["ALL=(ALL) NOPASSWD:ALL"] -+ shell: /bin/bash -+ distro: rhel -+ paths: -+ cloud_dir: /var/lib/cloud -+ templates_dir: /etc/cloud/templates -+ ssh_svcname: sshd -+ -+# vim:syntax=yaml --- -2.27.0 - diff --git a/cloud-init-22.1-PR1224-full-nm-support.patch b/cloud-init-22.1-PR1224-full-nm-support.patch deleted file mode 100644 index 827da0c..0000000 --- a/cloud-init-22.1-PR1224-full-nm-support.patch +++ /dev/null @@ -1,2442 +0,0 @@ -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 deleted file mode 100644 index 720fd58..0000000 --- a/cloud-init-22.1-nm-default.patch +++ /dev/null @@ -1,48 +0,0 @@ -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-22.3-nm-default.patch b/cloud-init-22.3-nm-default.patch new file mode 100644 index 0000000..fdca268 --- /dev/null +++ b/cloud-init-22.3-nm-default.patch @@ -0,0 +1,94 @@ +From 7703aa98b89c8daba207c28a0422268ead10019a Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Thu, 19 May 2022 15:05:01 +0200 +Subject: [PATCH] Use Network-Manager and Netplan as default renderers for RHEL + and Fedora (#1465) + +This is adapted from Neal Gompa's PR: +https://github.com/canonical/cloud-init/pull/1435 + +The only difference is that we are not modifying renderers.py (thus +modifying the priority of all distros), but just tweaking cloud.cfg to +apply this change to Fedora and RHEL. Other distros can optionally +add themselves afterwards. + + net: Prefer Netplan and NetworkManager renderers by default + + NetworkManager is used by default on a variety of Linux distributions, + and exists as a cross-distribution network management service. + + Additionally, add information about the NetworkManager renderer to + the cloud-init documentation. + + Because Netplan can be explicitly used to manage NetworkManager, + it needs to be preferred before NetworkManager. + + This change is a follow-up to #1224, which added the native + NetworkManager renderer. + This patch has been deployed on Fedora's cloud-init package throughout + the development of Fedora Linux 36 to verify that it works. + + This should also make it tremendously easier for Linux distributions + to use cloud-init because now a standard configuration is supported + by default. + + Signed-off-by: Neal Gompa + +Signed-off-by: Emanuele Giuseppe Esposito +--- + config/cloud.cfg.tmpl | 3 +++ + doc/rtd/topics/network-config.rst | 12 +++++++++++- + 2 files changed, 14 insertions(+), 1 deletion(-) + +diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl +index 6951a0e3..707a050c 100644 +--- a/config/cloud.cfg.tmpl ++++ b/config/cloud.cfg.tmpl +@@ -349,4 +349,7 @@ system_info: + {% elif variant in ["dragonfly"] %} + network: + renderers: ['freebsd'] ++{% elif variant in ["rhel", "fedora"] %} ++ network: ++ renderers: ['netplan', 'network-manager', 'networkd', 'sysconfig', 'eni'] + {% endif %} +diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst +index c461a3fe..f503caab 100644 +--- a/doc/rtd/topics/network-config.rst ++++ b/doc/rtd/topics/network-config.rst +@@ -188,6 +188,15 @@ generated configuration into an internal network configuration state. From + this state `Cloud-init`_ delegates rendering of the configuration to Distro + supported formats. The following ``renderers`` are supported in cloud-init: + ++- **NetworkManager** ++ ++`NetworkManager `_ is the standard Linux network ++configuration tool suite. It supports a wide range of networking setups. ++Configuration is typically stored in ``/etc/NetworkManager``. ++ ++It is the default for a number of Linux distributions, notably Fedora; ++CentOS/RHEL; and derivatives. ++ + - **ENI** + + /etc/network/interfaces or ``ENI`` is supported by the ``ifupdown`` package +@@ -215,6 +224,7 @@ is as follows: + - ENI + - Sysconfig + - Netplan ++- NetworkManager + + When applying the policy, `Cloud-init`_ checks if the current instance has the + correct binaries and paths to support the renderer. The first renderer that +@@ -223,7 +233,7 @@ supplying an updated configuration in cloud-config. :: + + system_info: + network: +- renderers: ['netplan', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] ++ renderers: ['netplan', 'network-manager', 'eni', 'sysconfig', 'freebsd', 'netbsd', 'openbsd'] + + + Network Configuration Tools +-- +2.36.1 + diff --git a/cloud-init.spec b/cloud-init.spec index 74fbf31..b1a81b3 100644 --- a/cloud-init.spec +++ b/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init -Version: 22.1 -Release: 3%{?dist} +Version: 22.2 +Release: 1%{?dist} Summary: Cloud instance init scripts License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -8,17 +8,10 @@ URL: http://launchpad.net/cloud-init Source0: https://launchpad.net/cloud-init/trunk/%{version}/+download/%{name}-%{version}.tar.gz Source1: cloud-init-tmpfiles.conf -# 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 - # 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 -Patch3: cloud-init-21.3-Adding-RHEL-default-cloud.cfg.patch +# From: https://github.com/canonical/cloud-init/commit/7703aa98b89c8daba207c28a0422268ead10019a +Patch1: cloud-init-22.3-nm-default.patch BuildArch: noarch @@ -30,6 +23,7 @@ BuildRequires: systemd # For tests BuildRequires: python3-pytest +BuildRequires: python3-pytest-mock BuildRequires: iproute BuildRequires: passwd BuildRequires: python3-configobj @@ -46,6 +40,7 @@ BuildRequires: python3-prettytable BuildRequires: python3-pyserial BuildRequires: python3-PyYAML BuildRequires: python3-requests +BuildRequires: python3-responses BuildRequires: python3-six BuildRequires: python3-netifaces # dnf is needed to make cc_ntp unit tests work @@ -109,11 +104,8 @@ find tests/ -type f | xargs sed -i s/assertItemsEqual/assertCountEqual/ %install %py3_install -- --init-system=systemd -%if 0%{?fedora} -python3 tools/render-cloudcfg --variant fedora > $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg -%elif 0%{?rhel} -cp -p rhel/cloud.cfg $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg -%endif +# Generate cloud-config file +python3 tools/render-cloudcfg --variant %{?rhel:rhel}%{!?rhel:fedora} > $RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg mkdir -p $RPM_BUILD_ROOT/var/lib/cloud @@ -188,6 +180,9 @@ python3 -m pytest tests/unittests %changelog +* Thu May 19 2022 Neal Gompa - 22.2-1 +- Rebase to 22.2 + * Thu Mar 10 2022 Dusty Mabe - 22.1-3 - Drop requirement on NetworkManager-config-server diff --git a/sources b/sources index e4f7158..14834d3 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (cloud-init-22.1.tar.gz) = 485e358777379a22dd2b0f6aa7afb1751eb44831c6e03ecbbd9c6823eaa20535e6e83fc245818ce1bb207425976839b356dadcfa3cfe62385b9d340b08ff21ab +SHA512 (cloud-init-22.2.tar.gz) = 07fec2f1d6eab20a1161672bb339a0c6b2826540bcb03936f95458b179fcb1b3142773c9a4038fe02b30bb05a5ca48a4153b6b0f59015b43bd6c6602832f9d6f