From c720ab9703752535767691a31e4720e11674bb1f Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Fri, 4 Aug 2023 08:58:26 +0530 Subject: [PATCH] NM renderer: set default IPv6 addr-gen-mode for all interfaces to eui64 (#4291) By default, NetworkManager renderer in cloud-init does not set any specific method for IPV6 addr-gen-mode in the keyfiles it writes. Hence, implicitly the mode is set to `eui64` in the absence of any global addr-gen-mode option in NetworkManager configuration. Later when other interfaces get added via D-Bus API or by using nmcli commands without explictly setting an addr-gen-mode, NM auto generates new profiles for those interfaces with addr-gen-mode set to `stable-privacy`. This introduces inconsistency of configurations between interfaces based on how they were added. This can cause problems for the customers. In this change, cloud-init overrides NetworkManager's preferred default of `stable-privacy` to use EUI64 using a drop in NetworkManager configuration file. This setting can be overriden by using global-connection-defaults setting in /etc/NetworkManager/NetworkManager.conf file. RHBZ: 2188388 Signed-off-by: Ani Sinha (cherry picked from commit d41264cb4297a4b143a23f3677d33b81fbfc6e8e) Conflicts: tests/unittests/test_net.py --- cloudinit/net/network_manager.py | 21 ++++++++ tests/unittests/test_net.py | 91 +++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py index ca216928..8047f796 100644 --- a/cloudinit/net/network_manager.py +++ b/cloudinit/net/network_manager.py @@ -21,6 +21,15 @@ from cloudinit.net.network_state import NetworkState NM_RUN_DIR = "/etc/NetworkManager" NM_LIB_DIR = "/usr/lib/NetworkManager" NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" +NM_IPV6_ADDR_GEN_CONF = """# This is generated by cloud-init. Do not edit. +# +[.config] + enable=nm-version-min:1.40 +[connection.30-cloud-init-ip6-addr-gen-mode] + # Select EUI64 to be used if the profile does not specify it. + ipv6.addr-gen-mode=0 + +""" LOG = logging.getLogger(__name__) @@ -368,6 +377,12 @@ class Renderer(renderer.Renderer): name = conn_filename(con_id, target) util.write_file(name, conn.dump(), 0o600) + # Select EUI64 to be used by default by NM for creating the address + # for use with RFC4862 IPv6 Stateless Address Autoconfiguration. + util.write_file( + cloud_init_nm_conf_filename(target), NM_IPV6_ADDR_GEN_CONF, 0o600 + ) + def conn_filename(con_id, target=None): target_con_dir = subp.target_path(target, NM_RUN_DIR) @@ -375,6 +390,12 @@ def conn_filename(con_id, target=None): return f"{target_con_dir}/system-connections/{con_file}" +def cloud_init_nm_conf_filename(target=None): + target_con_dir = subp.target_path(target, NM_RUN_DIR) + conf_file = "30-cloud-init-ip6-addr-gen-mode.conf" + return f"{target_con_dir}/conf.d/{conf_file}" + + def available(target=None): # TODO: Move `uses_systemd` to a more appropriate location # It is imported here to avoid circular import diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index fd656a57..d49da696 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -5679,9 +5679,25 @@ class TestNetworkManagerRendering(CiTestCase): with_logs = True scripts_dir = "/etc/NetworkManager/system-connections" + conf_dir = "/etc/NetworkManager/conf.d" expected_name = "expected_network_manager" + expected_conf_d = { + "30-cloud-init-ip6-addr-gen-mode.conf": textwrap.dedent( + """\ + # This is generated by cloud-init. Do not edit. + # + [.config] + enable=nm-version-min:1.40 + [connection.30-cloud-init-ip6-addr-gen-mode] + # Select EUI64 to be used if the profile does not specify it. + ipv6.addr-gen-mode=0 + + """ + ), + } + def _get_renderer(self): return network_manager.Renderer() @@ -5700,11 +5716,19 @@ class TestNetworkManagerRendering(CiTestCase): renderer.render_network_state(ns, target=dir) return dir2dict(dir) - def _compare_files_to_expected(self, expected, found): + def _compare_files_to_expected( + self, expected_scripts, expected_conf, found + ): orig_maxdiff = self.maxDiff - expected_d = dict( - (os.path.join(self.scripts_dir, k), v) for k, v in expected.items() + conf_d = dict( + (os.path.join(self.conf_dir, k), v) + for k, v in expected_conf.items() + ) + scripts_d = dict( + (os.path.join(self.scripts_dir, k), v) + for k, v in expected_scripts.items() ) + expected_d = {**conf_d, **scripts_d} try: self.maxDiff = None @@ -5765,6 +5789,7 @@ class TestNetworkManagerRendering(CiTestCase): """ ), }, + self.expected_conf_d, found, ) @@ -5820,8 +5845,9 @@ class TestNetworkManagerRendering(CiTestCase): gateway=10.0.2.2 """ - ), + ) }, + self.expected_conf_d, found, ) @@ -5857,33 +5883,44 @@ class TestNetworkManagerRendering(CiTestCase): """ ), }, + self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, found + ) self.assertNotIn( "WARNING: Network config: ignoring eth0.101 device-level mtu", self.logs.getvalue(), @@ -5892,12 +5929,16 @@ class TestNetworkManagerRendering(CiTestCase): 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, found + ) expected_msg = ( "WARNING: Network config: ignoring iface0 device-level mtu:8999" " because ipv4 subnet-level mtu:9000 provided." @@ -5907,41 +5948,55 @@ class TestNetworkManagerRendering(CiTestCase): 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, 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) + self._compare_files_to_expected( + entry[self.expected_name], self.expected_conf_d, found + ) @mock.patch(