From a5302001e9b1fe41256694f4a6481a278439c1e6 Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Fri, 15 Sep 2023 19:42:14 +0530 Subject: [PATCH] net: allow for NetworkManager configuration for NetworkManager net renderer This is the equivalent of the change made in the following commit for sysconfig renderer but this time it is for network_manager renderer: 67bab5bb804e2346673 ("net: Allow for NetworkManager configuration") The reasons for making the change has already been outlined in the above commit. When cloud-init configures DNS and produces /etc/resolv.conf, NetworkManager, if enabled, can clobber the above file and could also render it empty. Hence, this change makes cloud-init add a NetworkManager configuration so that it disables management and handling of /etc/resolv.conf by NetworkManager. Some code refactoring has been performed so as to avoid code duplication. Unit tests have been adjusted accordingly. LP: #1693251 Signed-off-by: Ani Sinha --- cloudinit/net/network_manager.py | 21 +++++++++++++++--- cloudinit/net/sysconfig.py | 38 +++++++++++++++----------------- tests/unittests/test_net.py | 22 +++++++++++++++--- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py index 8374cfcc878..281fb1a086c 100644 --- a/cloudinit/net/network_manager.py +++ b/cloudinit/net/network_manager.py @@ -17,6 +17,7 @@ from cloudinit import subp, util from cloudinit.net import is_ipv6_address, renderer, subnet_is_ipv6 from cloudinit.net.network_state import NetworkState +from cloudinit.net.sysconfig import NM_CONF_PATH, disable_nm_dns_handling NM_RUN_DIR = "/etc/NetworkManager" NM_LIB_DIR = "/usr/lib/NetworkManager" @@ -350,7 +351,7 @@ class Renderer(renderer.Renderer): def __init__(self, config=None): self.connections = {} - self.config = config + self.config = {} if not config else config def get_conn(self, con_id): return self.connections[con_id] @@ -389,9 +390,23 @@ def render_network_state( # 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 + cloud_init_nm_ip6_addr_gen_conf_filename(target), + NM_IPV6_ADDR_GEN_CONF, + 0o600, ) + nm_conf_filename = self.config.get( + "networkmanager_conf_path", NM_CONF_PATH + ) + + nm_conf_content = disable_nm_dns_handling(network_state, templates) + if nm_conf_content: + util.write_file( + f"{target}/{nm_conf_filename}", + nm_conf_content, + 0o600, + ) + def conn_filename(con_id, target=None): target_con_dir = subp.target_path(target, NM_RUN_DIR) @@ -399,7 +414,7 @@ def conn_filename(con_id, target=None): return f"{target_con_dir}/system-connections/{con_file}" -def cloud_init_nm_conf_filename(target=None): +def cloud_init_nm_ip6_addr_gen_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}" diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index dab26a26b1d..2620b2412ab 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -36,6 +36,7 @@ "TencentOS", "virtuozzo", ] +NM_CONF_PATH = "etc/NetworkManager/conf.d/99-cloud-init.conf" def _make_header(sep="#"): @@ -67,6 +68,21 @@ def _quote_value(value): return value +def disable_nm_dns_handling(network_state, templates=None): + content = networkmanager_conf.NetworkManagerConf("") + + # If DNS server information is provided, configure + # NetworkManager to not manage dns, so that /etc/resolv.conf + # does not get clobbered. + if network_state.dns_nameservers: + content.set_section_keypair("main", "dns", "none") + + if len(content) == 0: + return None + out = "".join([_make_header(), "\n", "\n".join(content.write()), "\n"]) + return out + + class ConfigMap: """Sysconfig like dictionary object.""" @@ -374,9 +390,8 @@ def __init__(self, config=None): "netrules_path", "etc/udev/rules.d/70-persistent-net.rules" ) self.dns_path = config.get("dns_path", "etc/resolv.conf") - nm_conf_path = "etc/NetworkManager/conf.d/99-cloud-init.conf" self.networkmanager_conf_path = config.get( - "networkmanager_conf_path", nm_conf_path + "networkmanager_conf_path", NM_CONF_PATH ) self.templates = { "control": config.get("control"), @@ -846,21 +861,6 @@ def _render_dns(network_state, existing_dns_path=None): content_str = header + "\n" + content_str return content_str - @staticmethod - def _render_networkmanager_conf(network_state, templates=None): - content = networkmanager_conf.NetworkManagerConf("") - - # If DNS server information is provided, configure - # NetworkManager to not manage dns, so that /etc/resolv.conf - # does not get clobbered. - if network_state.dns_nameservers: - content.set_section_keypair("main", "dns", "none") - - if len(content) == 0: - return None - out = "".join([_make_header(), "\n", "\n".join(content.write()), "\n"]) - return out - @classmethod def _render_bridge_interfaces(cls, network_state, iface_contents, flavor): bridge_key_map = { @@ -1006,9 +1006,7 @@ def render_network_state( nm_conf_path = subp.target_path( target, self.networkmanager_conf_path ) - nm_conf_content = self._render_networkmanager_conf( - network_state, templates - ) + nm_conf_content = disable_nm_dns_handling(network_state, templates) if nm_conf_content: util.write_file(nm_conf_path, nm_conf_content, file_mode) if self.netrules_path: diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 3e8f721f098..32f2c3203c7 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -5887,6 +5887,16 @@ class TestNetworkManagerRendering(CiTestCase): """ ), } + expected_nm_conf_d = { + "99-cloud-init.conf": textwrap.dedent( + """\ + # Created by cloud-init automatically, do not edit. + # + [main] + dns = none + """ + ), + } def _get_renderer(self): return network_manager.Renderer() @@ -6109,7 +6119,9 @@ 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], self.expected_conf_d, found + entry[self.expected_name], + {**self.expected_conf_d, **self.expected_nm_conf_d}, + found, ) self.assertNotIn( "WARNING: Network config: ignoring eth0.101 device-level mtu", @@ -6120,14 +6132,18 @@ def test_small_config_v1(self): entry = NETWORK_CONFIGS["small_v1"] found = self._render_and_read(network_config=yaml.load(entry["yaml"])) self._compare_files_to_expected( - entry[self.expected_name], self.expected_conf_d, found + entry[self.expected_name], + {**self.expected_conf_d, **self.expected_nm_conf_d}, + found, ) def test_small_config_v2(self): entry = NETWORK_CONFIGS["small_v2"] found = self._render_and_read(network_config=yaml.load(entry["yaml"])) self._compare_files_to_expected( - entry[self.expected_name], self.expected_conf_d, found + entry[self.expected_name], + {**self.expected_conf_d, **self.expected_nm_conf_d}, + found, ) def test_v4_and_v6_static_config(self):