From b1dd14ffafad2d2ca84326c525962b2ca086b292 Mon Sep 17 00:00:00 2001 From: Ani Sinha Date: Wed, 22 Mar 2023 16:31:58 +0530 Subject: [PATCH] Revert "Add native NetworkManager support (#1224)" This reverts commit feda344e6cf9d37b09bc13cf333a717d1654c26c. Signed-off-by: Ani Sinha --- cloudinit/cmd/devel/net_convert.py | 14 +- cloudinit/net/activators.py | 25 +- cloudinit/net/network_manager.py | 393 ---------------- cloudinit/net/renderers.py | 2 - cloudinit/net/sysconfig.py | 42 +- tests/unittests/test_net.py | 597 +++++-------------------- tests/unittests/test_net_activators.py | 11 +- 7 files changed, 161 insertions(+), 923 deletions(-) delete mode 100644 cloudinit/net/network_manager.py diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index eee49860..1a0a31ac 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -10,14 +10,7 @@ import sys import yaml from cloudinit import distros, log, safeyaml -from cloudinit.net import ( - eni, - netplan, - network_manager, - network_state, - networkd, - sysconfig, -) +from cloudinit.net import eni, netplan, network_state, networkd, sysconfig from cloudinit.sources import DataSourceAzure as azure from cloudinit.sources.helpers import openstack from cloudinit.sources.helpers.vmware.imc import guestcust_util @@ -84,7 +77,7 @@ def get_parser(parser=None): parser.add_argument( "-O", "--output-kind", - choices=["eni", "netplan", "networkd", "sysconfig", "network-manager"], + choices=["eni", "netplan", "networkd", "sysconfig"], required=True, help="The network config format to emit", ) @@ -157,9 +150,6 @@ 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 7d11a02c..d9a8c4d7 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -1,14 +1,15 @@ # 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 Dict, Iterable, List, Optional, Type, Union 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__) @@ -123,24 +124,20 @@ class IfUpDownActivator(NetworkActivator): class NetworkManagerActivator(NetworkActivator): @staticmethod def available(target=None) -> bool: - """Return true if NetworkManager can be used on this system.""" - return nm_available(target=target) + """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) @staticmethod def bring_up_interface(device_name: str) -> bool: - """Bring up connection using nmcli. + """Bring up interface using nmcli. Return True is successful, otherwise return False """ - 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] + cmd = ["nmcli", "connection", "up", "ifname", device_name] return _alter_interface(cmd, device_name) @staticmethod @@ -149,7 +146,7 @@ class NetworkManagerActivator(NetworkActivator): Return True is successful, otherwise return False """ - cmd = ["nmcli", "device", "disconnect", device_name] + cmd = ["nmcli", "connection", "down", device_name] return _alter_interface(cmd, device_name) diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py deleted file mode 100644 index 53763d15..00000000 --- a/cloudinit/net/network_manager.py +++ /dev/null @@ -1,393 +0,0 @@ -# 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 typing import Optional - -from cloudinit import log as logging -from cloudinit import subp, util -from cloudinit.net import is_ipv6_address, renderer, subnet_is_ipv6 -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" -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": "auto", - "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") - - 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_address(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: NetworkState, - templates: Optional[dict] = None, - target=None, - ) -> 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): - # TODO: Move `uses_systemd` to a more appropriate location - # It is imported here to avoid circular import - from cloudinit.distros import uses_systemd - - config_present = os.path.isfile(subp.target_path(target, path=NM_CFG_FILE)) - nmcli_present = subp.which("nmcli", target=target) - service_active = True - if uses_systemd(): - try: - subp.subp(["systemctl", "is-enabled", "NetworkManager.service"]) - except subp.ProcessExecutionError: - service_active = False - - return config_present and bool(nmcli_present) and service_active - - -# vi: ts=4 expandtab diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py index b241683f..c92b9dcf 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -8,7 +8,6 @@ from cloudinit.net import ( freebsd, netbsd, netplan, - network_manager, networkd, openbsd, renderer, @@ -20,7 +19,6 @@ NAME_TO_RENDERER = { "freebsd": freebsd, "netbsd": netbsd, "netplan": netplan, - "network-manager": network_manager, "networkd": networkd, "openbsd": openbsd, "sysconfig": sysconfig, diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 4262cd48..765c248a 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -6,6 +6,8 @@ import os import re from typing import Mapping, Optional +from configobj import ConfigObj + from cloudinit import log as logging from cloudinit import subp, util from cloudinit.distros.parsers import networkmanager_conf, resolv_conf @@ -35,7 +37,7 @@ KNOWN_DISTROS = [ "TencentOS", "virtuozzo", ] - +NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" def _make_header(sep="#"): lines = [ @@ -66,7 +68,26 @@ def _quote_value(value): return value -class ConfigMap: + +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.""" # Why does redhat prefer yes/no to true/false?? @@ -1014,6 +1035,8 @@ class Renderer(renderer.Renderer): 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 @@ -1052,9 +1075,14 @@ def _supported_vlan_names(rdev, vid): def available(target=None): - if not util.system_info()["variant"] in KNOWN_DISTROS: - return False + sysconfig = available_sysconfig(target=target) + nm = available_nm(target=target) + return util.system_info()["variant"] in KNOWN_DISTROS and any( + [nm, sysconfig] + ) + +def available_sysconfig(target=None): expected = ["ifup", "ifdown"] search = ["/sbin", "/usr/sbin"] for p in expected: @@ -1071,4 +1099,10 @@ def available(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 0f523ff8..4434b350 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -23,7 +23,6 @@ from cloudinit.net import ( mask_and_ipv4_to_bcast_addr, natural_sort_key, netplan, - network_manager, network_state, networkd, renderers, @@ -617,37 +616,6 @@ dns = none ), ), ], - "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": { @@ -1110,50 +1078,6 @@ NETWORK_CONFIGS = { 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 @@ -1959,29 +1883,6 @@ NETWORK_CONFIGS = { """ ), }, - "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 @@ -2035,30 +1936,6 @@ NETWORK_CONFIGS = { """ ), }, - "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 @@ -3092,8 +2969,8 @@ iface bond0 inet6 static - to: 2001:67c:1562:8007::1/64 via: 2001:67c:1562:8007::aac:40b2 - metric: 10000 - to: 3001:67c:15:8007::1/64 - via: 3001:67c:15:8007::aac:40b2 + to: 3001:67c:1562:8007::1/64 + via: 3001:67c:1562:8007::aac:40b2 """ ), "expected_netplan-v2": textwrap.dedent( @@ -3125,8 +3002,8 @@ iface bond0 inet6 static - to: 2001:67c:1562:8007::1/64 via: 2001:67c:1562:8007::aac:40b2 - metric: 10000 - to: 3001:67c:15:8007::1/64 - via: 3001:67c:15:8007::aac:40b2 + to: 3001:67c:1562:8007::1/64 + via: 3001:67c:1562:8007::aac:40b2 ethernets: eth0: match: @@ -3774,73 +3651,6 @@ iface bond0 inet6 static """ ), }, - "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 - - """ - ), - }, }, "v2-dev-name-via-mac-lookup": { "expected_sysconfig_rhel": { @@ -4339,6 +4149,7 @@ 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, " @@ -4913,6 +4724,78 @@ USERCTL=no 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) @@ -5609,281 +5492,6 @@ STARTMODE=auto 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") @@ -7651,9 +7259,9 @@ class TestNetworkdRoundTrip(CiTestCase): class TestRenderersSelect: @pytest.mark.parametrize( - "renderer_selected,netplan,eni,sys,network_manager,networkd", + "renderer_selected,netplan,eni,nm,scfg,sys,networkd", ( - # -netplan -ifupdown -sys -network-manager -networkd raises error + # -netplan -ifupdown -nm -scfg -sys raises error ( net.RendererNotFoundError, False, @@ -7661,51 +7269,52 @@ class TestRenderersSelect: False, False, False, + False, ), - # -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), + # -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), + # Ubuntu with Network-Manager installed + # +netplan -ifupdown +nm -scfg -sys selects netplan + ("netplan", True, False, 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), ), ) @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.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_network_manager_avail, m_networkd_avail, renderer_selected, netplan, eni, + nm, + scfg, sys, - network_manager, 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_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( @@ -7763,7 +7372,7 @@ class TestNetRenderers(CiTestCase): priority=["sysconfig", "eni"], ) - @mock.patch("cloudinit.net.sysconfig.available") + @mock.patch("cloudinit.net.sysconfig.available_sysconfig") @mock.patch("cloudinit.util.system_info") def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail): m_avail.return_value = True diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py index afd9056a..b735ea9e 100644 --- a/tests/unittests/test_net_activators.py +++ b/tests/unittests/test_net_activators.py @@ -139,6 +139,10 @@ NETPLAN_AVAILABLE_CALLS = [ (("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}), @@ -150,6 +154,7 @@ NETWORKD_AVAILABLE_CALLS = [ [ (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS), (NetplanActivator, NETPLAN_AVAILABLE_CALLS), + (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS), (NetworkdActivator, NETWORKD_AVAILABLE_CALLS), ], ) @@ -254,11 +259,9 @@ class TestActivatorsBringUp: def test_bring_up_interface( self, m_subp, activator, expected_call_list, available_mocks ): - index = 0 activator.bring_up_interface("eth0") - for call in m_subp.call_args_list: - assert call == expected_call_list[index] - index += 1 + assert len(m_subp.call_args_list) == 1 + assert m_subp.call_args_list[0] == expected_call_list[0] @patch("cloudinit.subp.subp", return_value=("", "")) def test_bring_up_interfaces(