284 lines
11 KiB
Diff
284 lines
11 KiB
Diff
|
From c720ab9703752535767691a31e4720e11674bb1f Mon Sep 17 00:00:00 2001
|
||
|
From: Ani Sinha <anisinha@redhat.com>
|
||
|
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 <anisinha@redhat.com>
|
||
|
(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(
|