59e311
From f0ae77cbf4a5e269da54fc2783a2a836023bbd86 Mon Sep 17 00:00:00 2001
59e311
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
59e311
Date: Mon, 2 May 2022 14:42:52 +0200
59e311
Subject: [PATCH 1/5] Add native NetworkManager support (#1224)
59e311
59e311
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
59e311
RH-MergeRequest: 24: Add native NetworkManager support (#1224)
59e311
RH-Commit: [1/3] 65231ba68460c505646807faf186c704d67678b5 (eesposit/cloud-init-centos-)
59e311
RH-Bugzilla: 2056964
59e311
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
59e311
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
59e311
59e311
commit feda344e6cf9d37b09bc13cf333a717d1654c26c
59e311
Author: Lubomir Rintel <lkundrak@v3.sk>
59e311
Date:   Fri Feb 25 23:33:20 2022 +0100
59e311
59e311
    Add native NetworkManager support (#1224)
59e311
59e311
    Fedora currently relies on sysconfig/ifcfg renderer. This is not too great,
59e311
    because Fedora (also RHEL since version 8) dropped support for the legacy
59e311
    network service that uses ifcfg files long ago.
59e311
59e311
    In turn, Fedora ended up patching cloud-init downstream to utilize
59e311
    NetworkManager's ifcfg compatibility mode [1]. This seems to have worked
59e311
    for a while, nevertheless the NetworkManager's ifcfg backend is reaching
59e311
    the end of its useful life too [2].
59e311
59e311
    [1] https://src.fedoraproject.org/rpms/cloud-init/blob/rawhide/f/cloud-init-21.3-nm-controlled.patch
59e311
    [2] https://fedoraproject.org/wiki/Changes/NoIfcfgFiles
59e311
59e311
    Let's not mangle things downstream and make vanilla cloud-init work great
59e311
    on Fedora instead.
59e311
59e311
    This also means that the sysconfig compatibility with
59e311
    Network Manager was removed.
59e311
59e311
    Firstly, this relies upon the fact that you can get ifcfg support by adding
59e311
    it to NetworkManager.conf. That is not guaranteed and certainly will not
59e311
    be case in future.
59e311
59e311
    Secondly, cloud-init always generates configuration with
59e311
    NM_CONTROLLED=no, so the generated ifcfg files are no good for
59e311
    NetworkManager. Fedora patches around this by just removing those lines
59e311
    in their cloud-init package.
59e311
59e311
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
59e311
---
59e311
 cloudinit/cmd/devel/net_convert.py     |   14 +-
59e311
 cloudinit/net/activators.py            |   25 +-
59e311
 cloudinit/net/network_manager.py       |  377 +++++++
59e311
 cloudinit/net/renderers.py             |    3 +
59e311
 cloudinit/net/sysconfig.py             |   37 +-
59e311
 tests/unittests/test_net.py            | 1270 +++++++++++++++++++++---
59e311
 tests/unittests/test_net_activators.py |   93 +-
59e311
 7 files changed, 1625 insertions(+), 194 deletions(-)
59e311
 create mode 100644 cloudinit/net/network_manager.py
59e311
59e311
diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py
59e311
index 18b1e7ff..647fe07b 100755
59e311
--- a/cloudinit/cmd/devel/net_convert.py
59e311
+++ b/cloudinit/cmd/devel/net_convert.py
59e311
@@ -7,7 +7,14 @@ import os
59e311
 import sys
59e311
 
59e311
 from cloudinit import distros, log, safeyaml
59e311
-from cloudinit.net import eni, netplan, network_state, networkd, sysconfig
59e311
+from cloudinit.net import (
59e311
+    eni,
59e311
+    netplan,
59e311
+    network_manager,
59e311
+    network_state,
59e311
+    networkd,
59e311
+    sysconfig,
59e311
+)
59e311
 from cloudinit.sources import DataSourceAzure as azure
59e311
 from cloudinit.sources import DataSourceOVF as ovf
59e311
 from cloudinit.sources.helpers import openstack
59e311
@@ -74,7 +81,7 @@ def get_parser(parser=None):
59e311
     parser.add_argument(
59e311
         "-O",
59e311
         "--output-kind",
59e311
-        choices=["eni", "netplan", "networkd", "sysconfig"],
59e311
+        choices=["eni", "netplan", "networkd", "sysconfig", "network-manager"],
59e311
         required=True,
59e311
         help="The network config format to emit",
59e311
     )
59e311
@@ -148,6 +155,9 @@ def handle_args(name, args):
59e311
     elif args.output_kind == "sysconfig":
59e311
         r_cls = sysconfig.Renderer
59e311
         config = distro.renderer_configs.get("sysconfig")
59e311
+    elif args.output_kind == "network-manager":
59e311
+        r_cls = network_manager.Renderer
59e311
+        config = distro.renderer_configs.get("network-manager")
59e311
     else:
59e311
         raise RuntimeError("Invalid output_kind")
59e311
 
59e311
diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py
59e311
index e80c26df..edbc0c06 100644
59e311
--- a/cloudinit/net/activators.py
59e311
+++ b/cloudinit/net/activators.py
59e311
@@ -1,15 +1,14 @@
59e311
 # This file is part of cloud-init. See LICENSE file for license information.
59e311
 import logging
59e311
-import os
59e311
 from abc import ABC, abstractmethod
59e311
 from typing import Iterable, List, Type
59e311
 
59e311
 from cloudinit import subp, util
59e311
 from cloudinit.net.eni import available as eni_available
59e311
 from cloudinit.net.netplan import available as netplan_available
59e311
+from cloudinit.net.network_manager import available as nm_available
59e311
 from cloudinit.net.network_state import NetworkState
59e311
 from cloudinit.net.networkd import available as networkd_available
59e311
-from cloudinit.net.sysconfig import NM_CFG_FILE
59e311
 
59e311
 LOG = logging.getLogger(__name__)
59e311
 
59e311
@@ -124,20 +123,24 @@ class IfUpDownActivator(NetworkActivator):
59e311
 class NetworkManagerActivator(NetworkActivator):
59e311
     @staticmethod
59e311
     def available(target=None) -> bool:
59e311
-        """Return true if network manager can be used on this system."""
59e311
-        config_present = os.path.isfile(
59e311
-            subp.target_path(target, path=NM_CFG_FILE)
59e311
-        )
59e311
-        nmcli_present = subp.which("nmcli", target=target)
59e311
-        return config_present and bool(nmcli_present)
59e311
+        """Return true if NetworkManager can be used on this system."""
59e311
+        return nm_available(target=target)
59e311
 
59e311
     @staticmethod
59e311
     def bring_up_interface(device_name: str) -> bool:
59e311
-        """Bring up interface using nmcli.
59e311
+        """Bring up connection using nmcli.
59e311
 
59e311
         Return True is successful, otherwise return False
59e311
         """
59e311
-        cmd = ["nmcli", "connection", "up", "ifname", device_name]
59e311
+        from cloudinit.net.network_manager import conn_filename
59e311
+
59e311
+        filename = conn_filename(device_name)
59e311
+        cmd = ["nmcli", "connection", "load", filename]
59e311
+        if _alter_interface(cmd, device_name):
59e311
+            cmd = ["nmcli", "connection", "up", "filename", filename]
59e311
+        else:
59e311
+            _alter_interface(["nmcli", "connection", "reload"], device_name)
59e311
+            cmd = ["nmcli", "connection", "up", "ifname", device_name]
59e311
         return _alter_interface(cmd, device_name)
59e311
 
59e311
     @staticmethod
59e311
@@ -146,7 +149,7 @@ class NetworkManagerActivator(NetworkActivator):
59e311
 
59e311
         Return True is successful, otherwise return False
59e311
         """
59e311
-        cmd = ["nmcli", "connection", "down", device_name]
59e311
+        cmd = ["nmcli", "device", "disconnect", device_name]
59e311
         return _alter_interface(cmd, device_name)
59e311
 
59e311
 
59e311
diff --git a/cloudinit/net/network_manager.py b/cloudinit/net/network_manager.py
59e311
new file mode 100644
59e311
index 00000000..79b0fe0b
59e311
--- /dev/null
59e311
+++ b/cloudinit/net/network_manager.py
59e311
@@ -0,0 +1,377 @@
59e311
+# Copyright 2022 Red Hat, Inc.
59e311
+#
59e311
+# Author: Lubomir Rintel <lkundrak@v3.sk>
59e311
+# Fixes and suggestions contributed by James Falcon, Neal Gompa,
59e311
+# Zbigniew Jędrzejewski-Szmek and Emanuele Giuseppe Esposito.
59e311
+#
59e311
+# This file is part of cloud-init. See LICENSE file for license information.
59e311
+
59e311
+import configparser
59e311
+import io
59e311
+import itertools
59e311
+import os
59e311
+import uuid
59e311
+
59e311
+from cloudinit import log as logging
59e311
+from cloudinit import subp, util
59e311
+
59e311
+from . import renderer
59e311
+from .network_state import is_ipv6_addr, subnet_is_ipv6
59e311
+
59e311
+NM_RUN_DIR = "/etc/NetworkManager"
59e311
+NM_LIB_DIR = "/usr/lib/NetworkManager"
59e311
+LOG = logging.getLogger(__name__)
59e311
+
59e311
+
59e311
+class NMConnection:
59e311
+    """Represents a NetworkManager connection profile."""
59e311
+
59e311
+    def __init__(self, con_id):
59e311
+        """
59e311
+        Initializes the connection with some very basic properties,
59e311
+        notably the UUID so that the connection can be referred to.
59e311
+        """
59e311
+
59e311
+        # Chosen by fair dice roll
59e311
+        CI_NM_UUID = uuid.UUID("a3924cb8-09e0-43e9-890b-77972a800108")
59e311
+
59e311
+        self.config = configparser.ConfigParser()
59e311
+        # Identity option name mapping, to achieve case sensitivity
59e311
+        self.config.optionxform = str
59e311
+
59e311
+        self.config["connection"] = {
59e311
+            "id": f"cloud-init {con_id}",
59e311
+            "uuid": str(uuid.uuid5(CI_NM_UUID, con_id)),
59e311
+        }
59e311
+
59e311
+        # This is not actually used anywhere, but may be useful in future
59e311
+        self.config["user"] = {
59e311
+            "org.freedesktop.NetworkManager.origin": "cloud-init"
59e311
+        }
59e311
+
59e311
+    def _set_default(self, section, option, value):
59e311
+        """
59e311
+        Sets a property unless it's already set, ensuring the section
59e311
+        exists.
59e311
+        """
59e311
+
59e311
+        if not self.config.has_section(section):
59e311
+            self.config[section] = {}
59e311
+        if not self.config.has_option(section, option):
59e311
+            self.config[section][option] = value
59e311
+
59e311
+    def _set_ip_method(self, family, subnet_type):
59e311
+        """
59e311
+        Ensures there's appropriate [ipv4]/[ipv6] for given family
59e311
+        appropriate for given configuration type
59e311
+        """
59e311
+
59e311
+        method_map = {
59e311
+            "static": "manual",
59e311
+            "dhcp6": "dhcp",
59e311
+            "ipv6_slaac": "auto",
59e311
+            "ipv6_dhcpv6-stateless": "auto",
59e311
+            "ipv6_dhcpv6-stateful": "auto",
59e311
+            "dhcp4": "auto",
59e311
+            "dhcp": "auto",
59e311
+        }
59e311
+
59e311
+        # Ensure we got an [ipvX] section
59e311
+        self._set_default(family, "method", "disabled")
59e311
+
59e311
+        try:
59e311
+            method = method_map[subnet_type]
59e311
+        except KeyError:
59e311
+            # What else can we do
59e311
+            method = "auto"
59e311
+            self.config[family]["may-fail"] = "true"
59e311
+
59e311
+        # Make sure we don't "downgrade" the method in case
59e311
+        # we got conflicting subnets (e.g. static along with dhcp)
59e311
+        if self.config[family]["method"] == "dhcp":
59e311
+            return
59e311
+        if self.config[family]["method"] == "auto" and method == "manual":
59e311
+            return
59e311
+
59e311
+        self.config[family]["method"] = method
59e311
+        self._set_default(family, "may-fail", "false")
59e311
+        if family == "ipv6":
59e311
+            self._set_default(family, "addr-gen-mode", "stable-privacy")
59e311
+
59e311
+    def _add_numbered(self, section, key_prefix, value):
59e311
+        """
59e311
+        Adds a numbered property, such as address<n> or route<n>, ensuring
59e311
+        the appropriate value gets used for <n>.
59e311
+        """
59e311
+
59e311
+        for index in itertools.count(1):
59e311
+            key = f"{key_prefix}{index}"
59e311
+            if not self.config.has_option(section, key):
59e311
+                self.config[section][key] = value
59e311
+                break
59e311
+
59e311
+    def _add_address(self, family, subnet):
59e311
+        """
59e311
+        Adds an ipv[46]address<n> property.
59e311
+        """
59e311
+
59e311
+        value = subnet["address"] + "/" + str(subnet["prefix"])
59e311
+        self._add_numbered(family, "address", value)
59e311
+
59e311
+    def _add_route(self, family, route):
59e311
+        """
59e311
+        Adds a ipv[46].route<n> property.
59e311
+        """
59e311
+
59e311
+        value = route["network"] + "/" + str(route["prefix"])
59e311
+        if "gateway" in route:
59e311
+            value = value + "," + route["gateway"]
59e311
+        self._add_numbered(family, "route", value)
59e311
+
59e311
+    def _add_nameserver(self, dns):
59e311
+        """
59e311
+        Extends the ipv[46].dns property with a name server.
59e311
+        """
59e311
+
59e311
+        # FIXME: the subnet contains IPv4 and IPv6 name server mixed
59e311
+        # together. We might be getting an IPv6 name server while
59e311
+        # we're dealing with an IPv4 subnet. Sort this out by figuring
59e311
+        # out the correct family and making sure a valid section exist.
59e311
+        family = "ipv6" if is_ipv6_addr(dns) else "ipv4"
59e311
+        self._set_default(family, "method", "disabled")
59e311
+
59e311
+        self._set_default(family, "dns", "")
59e311
+        self.config[family]["dns"] = self.config[family]["dns"] + dns + ";"
59e311
+
59e311
+    def _add_dns_search(self, family, dns_search):
59e311
+        """
59e311
+        Extends the ipv[46].dns-search property with a name server.
59e311
+        """
59e311
+
59e311
+        self._set_default(family, "dns-search", "")
59e311
+        self.config[family]["dns-search"] = (
59e311
+            self.config[family]["dns-search"] + ";".join(dns_search) + ";"
59e311
+        )
59e311
+
59e311
+    def con_uuid(self):
59e311
+        """
59e311
+        Returns the connection UUID
59e311
+        """
59e311
+        return self.config["connection"]["uuid"]
59e311
+
59e311
+    def valid(self):
59e311
+        """
59e311
+        Can this be serialized into a meaningful connection profile?
59e311
+        """
59e311
+        return self.config.has_option("connection", "type")
59e311
+
59e311
+    @staticmethod
59e311
+    def mac_addr(addr):
59e311
+        """
59e311
+        Sanitize a MAC address.
59e311
+        """
59e311
+        return addr.replace("-", ":").upper()
59e311
+
59e311
+    def render_interface(self, iface, renderer):
59e311
+        """
59e311
+        Integrate information from network state interface information
59e311
+        into the connection. Most of the work is done here.
59e311
+        """
59e311
+
59e311
+        # Initialize type & connectivity
59e311
+        _type_map = {
59e311
+            "physical": "ethernet",
59e311
+            "vlan": "vlan",
59e311
+            "bond": "bond",
59e311
+            "bridge": "bridge",
59e311
+            "infiniband": "infiniband",
59e311
+            "loopback": None,
59e311
+        }
59e311
+
59e311
+        if_type = _type_map[iface["type"]]
59e311
+        if if_type is None:
59e311
+            return
59e311
+        if "bond-master" in iface:
59e311
+            slave_type = "bond"
59e311
+        else:
59e311
+            slave_type = None
59e311
+
59e311
+        self.config["connection"]["type"] = if_type
59e311
+        if slave_type is not None:
59e311
+            self.config["connection"]["slave-type"] = slave_type
59e311
+            self.config["connection"]["master"] = renderer.con_ref(
59e311
+                iface[slave_type + "-master"]
59e311
+            )
59e311
+
59e311
+        # Add type specific-section
59e311
+        self.config[if_type] = {}
59e311
+
59e311
+        # These are the interface properties that map nicely
59e311
+        # to NetworkManager properties
59e311
+        _prop_map = {
59e311
+            "bond": {
59e311
+                "mode": "bond-mode",
59e311
+                "miimon": "bond_miimon",
59e311
+                "xmit_hash_policy": "bond-xmit-hash-policy",
59e311
+                "num_grat_arp": "bond-num-grat-arp",
59e311
+                "downdelay": "bond-downdelay",
59e311
+                "updelay": "bond-updelay",
59e311
+                "fail_over_mac": "bond-fail-over-mac",
59e311
+                "primary_reselect": "bond-primary-reselect",
59e311
+                "primary": "bond-primary",
59e311
+            },
59e311
+            "bridge": {
59e311
+                "stp": "bridge_stp",
59e311
+                "priority": "bridge_bridgeprio",
59e311
+            },
59e311
+            "vlan": {
59e311
+                "id": "vlan_id",
59e311
+            },
59e311
+            "ethernet": {},
59e311
+            "infiniband": {},
59e311
+        }
59e311
+
59e311
+        device_mtu = iface["mtu"]
59e311
+        ipv4_mtu = None
59e311
+
59e311
+        # Deal with Layer 3 configuration
59e311
+        for subnet in iface["subnets"]:
59e311
+            family = "ipv6" if subnet_is_ipv6(subnet) else "ipv4"
59e311
+
59e311
+            self._set_ip_method(family, subnet["type"])
59e311
+            if "address" in subnet:
59e311
+                self._add_address(family, subnet)
59e311
+            if "gateway" in subnet:
59e311
+                self.config[family]["gateway"] = subnet["gateway"]
59e311
+            for route in subnet["routes"]:
59e311
+                self._add_route(family, route)
59e311
+            if "dns_nameservers" in subnet:
59e311
+                for nameserver in subnet["dns_nameservers"]:
59e311
+                    self._add_nameserver(nameserver)
59e311
+            if "dns_search" in subnet:
59e311
+                self._add_dns_search(family, subnet["dns_search"])
59e311
+            if family == "ipv4" and "mtu" in subnet:
59e311
+                ipv4_mtu = subnet["mtu"]
59e311
+
59e311
+        if ipv4_mtu is None:
59e311
+            ipv4_mtu = device_mtu
59e311
+        if not ipv4_mtu == device_mtu:
59e311
+            LOG.warning(
59e311
+                "Network config: ignoring %s device-level mtu:%s"
59e311
+                " because ipv4 subnet-level mtu:%s provided.",
59e311
+                iface["name"],
59e311
+                device_mtu,
59e311
+                ipv4_mtu,
59e311
+            )
59e311
+
59e311
+        # Parse type-specific properties
59e311
+        for nm_prop, key in _prop_map[if_type].items():
59e311
+            if key not in iface:
59e311
+                continue
59e311
+            if iface[key] is None:
59e311
+                continue
59e311
+            if isinstance(iface[key], bool):
59e311
+                self.config[if_type][nm_prop] = (
59e311
+                    "true" if iface[key] else "false"
59e311
+                )
59e311
+            else:
59e311
+                self.config[if_type][nm_prop] = str(iface[key])
59e311
+
59e311
+        # These ones need special treatment
59e311
+        if if_type == "ethernet":
59e311
+            if iface["wakeonlan"] is True:
59e311
+                # NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC
59e311
+                self.config["ethernet"]["wake-on-lan"] = str(0x40)
59e311
+            if ipv4_mtu is not None:
59e311
+                self.config["ethernet"]["mtu"] = str(ipv4_mtu)
59e311
+            if iface["mac_address"] is not None:
59e311
+                self.config["ethernet"]["mac-address"] = self.mac_addr(
59e311
+                    iface["mac_address"]
59e311
+                )
59e311
+        if if_type == "vlan" and "vlan-raw-device" in iface:
59e311
+            self.config["vlan"]["parent"] = renderer.con_ref(
59e311
+                iface["vlan-raw-device"]
59e311
+            )
59e311
+        if if_type == "bridge":
59e311
+            # Bridge is ass-backwards compared to bond
59e311
+            for port in iface["bridge_ports"]:
59e311
+                port = renderer.get_conn(port)
59e311
+                port._set_default("connection", "slave-type", "bridge")
59e311
+                port._set_default("connection", "master", self.con_uuid())
59e311
+            if iface["mac_address"] is not None:
59e311
+                self.config["bridge"]["mac-address"] = self.mac_addr(
59e311
+                    iface["mac_address"]
59e311
+                )
59e311
+        if if_type == "infiniband" and ipv4_mtu is not None:
59e311
+            self.config["infiniband"]["transport-mode"] = "datagram"
59e311
+            self.config["infiniband"]["mtu"] = str(ipv4_mtu)
59e311
+            if iface["mac_address"] is not None:
59e311
+                self.config["infiniband"]["mac-address"] = self.mac_addr(
59e311
+                    iface["mac_address"]
59e311
+                )
59e311
+
59e311
+        # Finish up
59e311
+        if if_type == "bridge" or not self.config.has_option(
59e311
+            if_type, "mac-address"
59e311
+        ):
59e311
+            self.config["connection"]["interface-name"] = iface["name"]
59e311
+
59e311
+    def dump(self):
59e311
+        """
59e311
+        Stringify.
59e311
+        """
59e311
+
59e311
+        buf = io.StringIO()
59e311
+        self.config.write(buf, space_around_delimiters=False)
59e311
+        header = "# Generated by cloud-init. Changes will be lost.\n\n"
59e311
+        return header + buf.getvalue()
59e311
+
59e311
+
59e311
+class Renderer(renderer.Renderer):
59e311
+    """Renders network information in a NetworkManager keyfile format."""
59e311
+
59e311
+    def __init__(self, config=None):
59e311
+        self.connections = {}
59e311
+
59e311
+    def get_conn(self, con_id):
59e311
+        return self.connections[con_id]
59e311
+
59e311
+    def con_ref(self, con_id):
59e311
+        if con_id in self.connections:
59e311
+            return self.connections[con_id].con_uuid()
59e311
+        else:
59e311
+            # Well, what can we do...
59e311
+            return con_id
59e311
+
59e311
+    def render_network_state(self, network_state, templates=None, target=None):
59e311
+        # First pass makes sure there's NMConnections for all known
59e311
+        # interfaces that have UUIDs that can be linked to from related
59e311
+        # interfaces
59e311
+        for iface in network_state.iter_interfaces():
59e311
+            self.connections[iface["name"]] = NMConnection(iface["name"])
59e311
+
59e311
+        # Now render the actual interface configuration
59e311
+        for iface in network_state.iter_interfaces():
59e311
+            conn = self.connections[iface["name"]]
59e311
+            conn.render_interface(iface, self)
59e311
+
59e311
+        # And finally write the files
59e311
+        for con_id, conn in self.connections.items():
59e311
+            if not conn.valid():
59e311
+                continue
59e311
+            name = conn_filename(con_id, target)
59e311
+            util.write_file(name, conn.dump(), 0o600)
59e311
+
59e311
+
59e311
+def conn_filename(con_id, target=None):
59e311
+    target_con_dir = subp.target_path(target, NM_RUN_DIR)
59e311
+    con_file = f"cloud-init-{con_id}.nmconnection"
59e311
+    return f"{target_con_dir}/system-connections/{con_file}"
59e311
+
59e311
+
59e311
+def available(target=None):
59e311
+    target_nm_dir = subp.target_path(target, NM_LIB_DIR)
59e311
+    return os.path.exists(target_nm_dir)
59e311
+
59e311
+
59e311
+# vi: ts=4 expandtab
59e311
diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
59e311
index c755f04c..7edc34b5 100644
59e311
--- a/cloudinit/net/renderers.py
59e311
+++ b/cloudinit/net/renderers.py
59e311
@@ -8,6 +8,7 @@ from . import (
59e311
     freebsd,
59e311
     netbsd,
59e311
     netplan,
59e311
+    network_manager,
59e311
     networkd,
59e311
     openbsd,
59e311
     renderer,
59e311
@@ -19,6 +20,7 @@ NAME_TO_RENDERER = {
59e311
     "freebsd": freebsd,
59e311
     "netbsd": netbsd,
59e311
     "netplan": netplan,
59e311
+    "network-manager": network_manager,
59e311
     "networkd": networkd,
59e311
     "openbsd": openbsd,
59e311
     "sysconfig": sysconfig,
59e311
@@ -28,6 +30,7 @@ DEFAULT_PRIORITY = [
59e311
     "eni",
59e311
     "sysconfig",
59e311
     "netplan",
59e311
+    "network-manager",
59e311
     "freebsd",
59e311
     "netbsd",
59e311
     "openbsd",
59e311
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
59e311
index b50035b5..2a45a4fa 100644
59e311
--- a/cloudinit/net/sysconfig.py
59e311
+++ b/cloudinit/net/sysconfig.py
59e311
@@ -5,8 +5,6 @@ import io
59e311
 import os
59e311
 import re
59e311
 
59e311
-from configobj import ConfigObj
59e311
-
59e311
 from cloudinit import log as logging
59e311
 from cloudinit import subp, util
59e311
 from cloudinit.distros.parsers import networkmanager_conf, resolv_conf
59e311
@@ -66,24 +64,6 @@ def _quote_value(value):
59e311
         return value
59e311
 
59e311
 
59e311
-def enable_ifcfg_rh(path):
59e311
-    """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present"""
59e311
-    config = ConfigObj(path)
59e311
-    if "main" in config:
59e311
-        if "plugins" in config["main"]:
59e311
-            if "ifcfg-rh" in config["main"]["plugins"]:
59e311
-                return
59e311
-        else:
59e311
-            config["main"]["plugins"] = []
59e311
-
59e311
-        if isinstance(config["main"]["plugins"], list):
59e311
-            config["main"]["plugins"].append("ifcfg-rh")
59e311
-        else:
59e311
-            config["main"]["plugins"] = [config["main"]["plugins"], "ifcfg-rh"]
59e311
-        config.write()
59e311
-        LOG.debug("Enabled ifcfg-rh NetworkManager plugins")
59e311
-
59e311
-
59e311
 class ConfigMap(object):
59e311
     """Sysconfig like dictionary object."""
59e311
 
59e311
@@ -1032,8 +1012,6 @@ class Renderer(renderer.Renderer):
59e311
             netrules_content = self._render_persistent_net(network_state)
59e311
             netrules_path = subp.target_path(target, self.netrules_path)
59e311
             util.write_file(netrules_path, netrules_content, file_mode)
59e311
-        if available_nm(target=target):
59e311
-            enable_ifcfg_rh(subp.target_path(target, path=NM_CFG_FILE))
59e311
 
59e311
         sysconfig_path = subp.target_path(target, templates.get("control"))
59e311
         # Distros configuring /etc/sysconfig/network as a file e.g. Centos
59e311
@@ -1072,14 +1050,9 @@ def _supported_vlan_names(rdev, vid):
59e311
 
59e311
 
59e311
 def available(target=None):
59e311
-    sysconfig = available_sysconfig(target=target)
59e311
-    nm = available_nm(target=target)
59e311
-    return util.system_info()["variant"] in KNOWN_DISTROS and any(
59e311
-        [nm, sysconfig]
59e311
-    )
59e311
-
59e311
+    if not util.system_info()["variant"] in KNOWN_DISTROS:
59e311
+        return False
59e311
 
59e311
-def available_sysconfig(target=None):
59e311
     expected = ["ifup", "ifdown"]
59e311
     search = ["/sbin", "/usr/sbin"]
59e311
     for p in expected:
59e311
@@ -1096,10 +1069,4 @@ def available_sysconfig(target=None):
59e311
     return False
59e311
 
59e311
 
59e311
-def available_nm(target=None):
59e311
-    if not os.path.isfile(subp.target_path(target, path=NM_CFG_FILE)):
59e311
-        return False
59e311
-    return True
59e311
-
59e311
-
59e311
 # vi: ts=4 expandtab
59e311
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
59e311
index 591241b3..ef21ad76 100644
59e311
--- a/tests/unittests/test_net.py
59e311
+++ b/tests/unittests/test_net.py
59e311
@@ -21,6 +21,7 @@ from cloudinit.net import (
59e311
     interface_has_own_mac,
59e311
     natural_sort_key,
59e311
     netplan,
59e311
+    network_manager,
59e311
     network_state,
59e311
     networkd,
59e311
     renderers,
59e311
@@ -611,6 +612,37 @@ dns = none
59e311
                 ),
59e311
             ),
59e311
         ],
59e311
+        "expected_network_manager": [
59e311
+            (
59e311
+                "".join(
59e311
+                    [
59e311
+                        "etc/NetworkManager/system-connections",
59e311
+                        "/cloud-init-eth0.nmconnection",
59e311
+                    ]
59e311
+                ),
59e311
+                """
59e311
+# Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+[connection]
59e311
+id=cloud-init eth0
59e311
+uuid=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+type=ethernet
59e311
+
59e311
+[user]
59e311
+org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+[ethernet]
59e311
+mac-address=FA:16:3E:ED:9A:59
59e311
+
59e311
+[ipv4]
59e311
+method=manual
59e311
+may-fail=false
59e311
+address1=172.19.1.34/22
59e311
+route1=0.0.0.0/0,172.19.3.254
59e311
+
59e311
+""".lstrip(),
59e311
+            ),
59e311
+        ],
59e311
     },
59e311
     {
59e311
         "in_data": {
59e311
@@ -1073,6 +1105,50 @@ NETWORK_CONFIGS = {
59e311
                 USERCTL=no"""
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-eth1.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth1
59e311
+                uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=CF:D6:AF:48:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth99.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth99
59e311
+                uuid=b1b88000-1f03-5360-8377-1a2205efffb4
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=C0:D6:9F:2C:E8:80
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+                address1=192.168.21.3/24
59e311
+                route1=0.0.0.0/0,65.61.151.37
59e311
+                dns=8.8.8.8;8.8.4.4;
59e311
+                dns-search=barley.maas;sach.maas;
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml": textwrap.dedent(
59e311
             """
59e311
             version: 1
59e311
@@ -1145,6 +1221,34 @@ NETWORK_CONFIGS = {
59e311
                 STARTMODE=auto"""
59e311
             )
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                [ipv6]
59e311
+                method=dhcp
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml": textwrap.dedent(
59e311
             """\
59e311
             version: 1
59e311
@@ -1247,6 +1351,37 @@ NETWORK_CONFIGS = {
59e311
                 """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mtu=9000
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.14.2/24
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::1/64
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "v6_and_v4": {
59e311
         "expected_sysconfig_opensuse": {
59e311
@@ -1257,6 +1392,34 @@ NETWORK_CONFIGS = {
59e311
                 STARTMODE=auto"""
59e311
             )
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv6]
59e311
+                method=dhcp
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml": textwrap.dedent(
59e311
             """\
59e311
             version: 1
59e311
@@ -1330,6 +1493,30 @@ NETWORK_CONFIGS = {
59e311
                 """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv6]
59e311
+                method=dhcp
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "dhcpv6_accept_ra": {
59e311
         "expected_eni": textwrap.dedent(
59e311
@@ -1537,6 +1724,30 @@ NETWORK_CONFIGS = {
59e311
             """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv6]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "static6": {
59e311
         "yaml": textwrap.dedent(
59e311
@@ -1625,6 +1836,30 @@ NETWORK_CONFIGS = {
59e311
             """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv6]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "dhcpv6_stateful": {
59e311
         "expected_eni": textwrap.dedent(
59e311
@@ -1724,6 +1959,29 @@ NETWORK_CONFIGS = {
59e311
             """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml_v2": textwrap.dedent(
59e311
             """\
59e311
             version: 2
59e311
@@ -1777,6 +2035,30 @@ NETWORK_CONFIGS = {
59e311
             """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-iface0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init iface0
59e311
+                uuid=8ddfba48-857c-5e86-ac09-1b43eae0bf70
59e311
+                type=ethernet
59e311
+                interface-name=iface0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                wake-on-lan=64
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml_v2": textwrap.dedent(
59e311
             """\
59e311
             version: 2
59e311
@@ -2215,6 +2497,254 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
59e311
                 USERCTL=no"""
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-eth3.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth3
59e311
+                uuid=b7e95dda-7746-5bf8-bf33-6e5f3c926790
59e311
+                type=ethernet
59e311
+                slave-type=bridge
59e311
+                master=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=66:BB:9F:2C:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth5.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth5
59e311
+                uuid=5fda13c7-9942-5e90-a41b-1d043bd725dc
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=98:BB:9F:2C:E8:8A
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-ib0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init ib0
59e311
+                uuid=11a1dda7-78b4-5529-beba-d9b5f549ad7b
59e311
+                type=infiniband
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [infiniband]
59e311
+                transport-mode=datagram
59e311
+                mtu=9000
59e311
+                mac-address=A0:00:02:20:FE:80:00:00:00:00:00:00:EC:0D:9A:03:00:15:E2:C1
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.200.7/24
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-bond0.200.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init bond0.200
59e311
+                uuid=88984a9c-ff22-5233-9267-86315e0acaa7
59e311
+                type=vlan
59e311
+                interface-name=bond0.200
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [vlan]
59e311
+                id=200
59e311
+                parent=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth0
59e311
+                uuid=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=C0:D6:9F:2C:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth4.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth4
59e311
+                uuid=e27e4959-fb50-5580-b9a4-2073554627b9
59e311
+                type=ethernet
59e311
+                slave-type=bridge
59e311
+                master=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=98:BB:9F:2C:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth1.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth1
59e311
+                uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
59e311
+                type=ethernet
59e311
+                slave-type=bond
59e311
+                master=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=AA:D6:9F:2C:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-br0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init br0
59e311
+                uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+                type=bridge
59e311
+                interface-name=br0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [bridge]
59e311
+                stp=false
59e311
+                priority=22
59e311
+                mac-address=BB:BB:BB:BB:BB:AA
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.14.2/24
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::1/64
59e311
+                route1=::/0,2001:4800:78ff:1b::1
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth0.101.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth0.101
59e311
+                uuid=b5acec5e-db80-5935-8b02-0d5619fc42bf
59e311
+                type=vlan
59e311
+                interface-name=eth0.101
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [vlan]
59e311
+                id=101
59e311
+                parent=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.0.2/24
59e311
+                gateway=192.168.0.1
59e311
+                dns=192.168.0.10;10.23.23.134;
59e311
+                dns-search=barley.maas;sacchromyces.maas;brettanomyces.maas;
59e311
+                address2=192.168.2.10/24
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-bond0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init bond0
59e311
+                uuid=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+                type=bond
59e311
+                interface-name=bond0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [bond]
59e311
+                mode=active-backup
59e311
+                miimon=100
59e311
+                xmit_hash_policy=layer3+4
59e311
+
59e311
+                [ipv6]
59e311
+                method=dhcp
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth2.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth2
59e311
+                uuid=5559a242-3421-5fdd-896e-9cb8313d5804
59e311
+                type=ethernet
59e311
+                slave-type=bond
59e311
+                master=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=C0:BB:9F:2C:E8:80
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
         "yaml": textwrap.dedent(
59e311
             """
59e311
             version: 1
59e311
@@ -2403,10 +2933,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
59e311
                   - type: static
59e311
                     address: 2001:1::1/92
59e311
                     routes:
59e311
-                        - gateway: 2001:67c:1562:1
59e311
+                        - gateway: 2001:67c:1562::1
59e311
                           network: 2001:67c:1
59e311
                           netmask: "ffff:ffff::"
59e311
-                        - gateway: 3001:67c:1562:1
59e311
+                        - gateway: 3001:67c:15::1
59e311
                           network: 3001:67c:1
59e311
                           netmask: "ffff:ffff::"
59e311
                           metric: 10000
59e311
@@ -2451,10 +2981,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
59e311
                      -   to: 10.1.3.0/24
59e311
                          via: 192.168.0.3
59e311
                      -   to: 2001:67c:1/32
59e311
-                         via: 2001:67c:1562:1
59e311
+                         via: 2001:67c:1562::1
59e311
                      -   metric: 10000
59e311
                          to: 3001:67c:1/32
59e311
-                         via: 3001:67c:1562:1
59e311
+                         via: 3001:67c:15::1
59e311
         """
59e311
         ),
59e311
         "expected_eni": textwrap.dedent(
59e311
@@ -2514,11 +3044,11 @@ iface bond0 inet static
59e311
 # control-alias bond0
59e311
 iface bond0 inet6 static
59e311
     address 2001:1::1/92
59e311
-    post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true
59e311
-    pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true
59e311
-    post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \
59e311
+    post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true
59e311
+    pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562::1 || true
59e311
+    post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \
59e311
 || true
59e311
-    pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \
59e311
+    pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:15::1 metric 10000 \
59e311
 || true
59e311
         """
59e311
         ),
59e311
@@ -2561,8 +3091,8 @@ iface bond0 inet6 static
59e311
                 -   to: 2001:67c:1562:8007::1/64
59e311
                     via: 2001:67c:1562:8007::aac:40b2
59e311
                 -   metric: 10000
59e311
-                    to: 3001:67c:1562:8007::1/64
59e311
-                    via: 3001:67c:1562:8007::aac:40b2
59e311
+                    to: 3001:67c:15:8007::1/64
59e311
+                    via: 3001:67c:15:8007::aac:40b2
59e311
             """
59e311
         ),
59e311
         "expected_netplan-v2": textwrap.dedent(
59e311
@@ -2594,8 +3124,8 @@ iface bond0 inet6 static
59e311
                      -   to: 2001:67c:1562:8007::1/64
59e311
                          via: 2001:67c:1562:8007::aac:40b2
59e311
                      -   metric: 10000
59e311
-                         to: 3001:67c:1562:8007::1/64
59e311
-                         via: 3001:67c:1562:8007::aac:40b2
59e311
+                         to: 3001:67c:15:8007::1/64
59e311
+                         via: 3001:67c:15:8007::aac:40b2
59e311
              ethernets:
59e311
                  eth0:
59e311
                      match:
59e311
@@ -2694,8 +3224,8 @@ iface bond0 inet6 static
59e311
                 """\
59e311
         # Created by cloud-init on instance boot automatically, do not edit.
59e311
         #
59e311
-        2001:67c:1/32 via 2001:67c:1562:1  dev bond0
59e311
-        3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0
59e311
+        2001:67c:1/32 via 2001:67c:1562::1  dev bond0
59e311
+        3001:67c:1/32 via 3001:67c:15::1 metric 10000 dev bond0
59e311
             """
59e311
             ),
59e311
             "route-bond0": textwrap.dedent(
59e311
@@ -2718,6 +3248,88 @@ iface bond0 inet6 static
59e311
         """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-bond0s0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init bond0s0
59e311
+                uuid=09d0b5b9-67e7-5577-a1af-74d1cf17a71e
59e311
+                type=ethernet
59e311
+                slave-type=bond
59e311
+                master=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=AA:BB:CC:DD:E8:00
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-bond0s1.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init bond0s1
59e311
+                uuid=4d9aca96-b515-5630-ad83-d13daac7f9d0
59e311
+                type=ethernet
59e311
+                slave-type=bond
59e311
+                master=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=AA:BB:CC:DD:E8:01
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-bond0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init bond0
59e311
+                uuid=54317911-f840-516b-a10d-82cb4c1f075c
59e311
+                type=bond
59e311
+                interface-name=bond0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [bond]
59e311
+                mode=active-backup
59e311
+                miimon=100
59e311
+                xmit_hash_policy=layer3+4
59e311
+                num_grat_arp=5
59e311
+                downdelay=10
59e311
+                updelay=20
59e311
+                fail_over_mac=active
59e311
+                primary_reselect=always
59e311
+                primary=bond0s0
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.0.2/24
59e311
+                gateway=192.168.0.1
59e311
+                route1=10.1.3.0/24,192.168.0.3
59e311
+                address2=192.168.1.2/24
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::1/92
59e311
+                route1=2001:67c:1/32,2001:67c:1562::1
59e311
+                route2=3001:67c:1/32,3001:67c:15::1
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "vlan": {
59e311
         "yaml": textwrap.dedent(
59e311
@@ -2801,6 +3413,58 @@ iface bond0 inet6 static
59e311
                 VLAN=yes"""
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-en0.99.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init en0.99
59e311
+                uuid=f594e2ed-f107-51df-b225-1dc530a5356b
59e311
+                type=vlan
59e311
+                interface-name=en0.99
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [vlan]
59e311
+                id=99
59e311
+                parent=e0ca478b-8d84-52ab-8fae-628482c629b5
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.2.2/24
59e311
+                address2=192.168.1.2/24
59e311
+                gateway=192.168.1.1
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::bbbb/96
59e311
+                route1=::/0,2001:1::1
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-en0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init en0
59e311
+                uuid=e0ca478b-8d84-52ab-8fae-628482c629b5
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=AA:BB:CC:DD:E8:00
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "bridge": {
59e311
         "yaml": textwrap.dedent(
59e311
@@ -2909,6 +3573,82 @@ iface bond0 inet6 static
59e311
                 """
59e311
             ),
59e311
         },
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-br0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init br0
59e311
+                uuid=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+                type=bridge
59e311
+                interface-name=br0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [bridge]
59e311
+                stp=false
59e311
+                priority=22
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.2.2/24
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth0
59e311
+                uuid=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+                type=ethernet
59e311
+                slave-type=bridge
59e311
+                master=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=52:54:00:12:34:00
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::100/96
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth1.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth1
59e311
+                uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
59e311
+                type=ethernet
59e311
+                slave-type=bridge
59e311
+                master=dee46ce4-af7a-5e7c-aa08-b25533ae9213
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=52:54:00:12:34:01
59e311
+
59e311
+                [ipv6]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                addr-gen-mode=stable-privacy
59e311
+                address1=2001:1::101/96
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
     },
59e311
     "manual": {
59e311
         "yaml": textwrap.dedent(
59e311
@@ -3037,28 +3777,95 @@ iface bond0 inet6 static
59e311
                 """
59e311
             ),
59e311
         },
59e311
-    },
59e311
-}
59e311
+        "expected_network_manager": {
59e311
+            "cloud-init-eth0.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
 
59e311
+                [connection]
59e311
+                id=cloud-init eth0
59e311
+                uuid=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+                type=ethernet
59e311
 
59e311
-CONFIG_V1_EXPLICIT_LOOPBACK = {
59e311
-    "version": 1,
59e311
-    "config": [
59e311
-        {
59e311
-            "name": "eth0",
59e311
-            "type": "physical",
59e311
-            "subnets": [{"control": "auto", "type": "dhcp"}],
59e311
-        },
59e311
-        {
59e311
-            "name": "lo",
59e311
-            "type": "loopback",
59e311
-            "subnets": [{"control": "auto", "type": "loopback"}],
59e311
-        },
59e311
-    ],
59e311
-}
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
 
59e311
+                [ethernet]
59e311
+                mac-address=52:54:00:12:34:00
59e311
 
59e311
-CONFIG_V1_SIMPLE_SUBNET = {
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=192.168.1.2/24
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth1.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth1
59e311
+                uuid=3c50eb47-7260-5a6d-801d-bd4f587d6b58
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mtu=1480
59e311
+                mac-address=52:54:00:12:34:AA
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=true
59e311
+
59e311
+                """
59e311
+            ),
59e311
+            "cloud-init-eth2.nmconnection": textwrap.dedent(
59e311
+                """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth2
59e311
+                uuid=5559a242-3421-5fdd-896e-9cb8313d5804
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=52:54:00:12:34:FF
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=true
59e311
+
59e311
+                """
59e311
+            ),
59e311
+        },
59e311
+    },
59e311
+}
59e311
+
59e311
+
59e311
+CONFIG_V1_EXPLICIT_LOOPBACK = {
59e311
+    "version": 1,
59e311
+    "config": [
59e311
+        {
59e311
+            "name": "eth0",
59e311
+            "type": "physical",
59e311
+            "subnets": [{"control": "auto", "type": "dhcp"}],
59e311
+        },
59e311
+        {
59e311
+            "name": "lo",
59e311
+            "type": "loopback",
59e311
+            "subnets": [{"control": "auto", "type": "loopback"}],
59e311
+        },
59e311
+    ],
59e311
+}
59e311
+
59e311
+
59e311
+CONFIG_V1_SIMPLE_SUBNET = {
59e311
     "version": 1,
59e311
     "config": [
59e311
         {
59e311
@@ -3497,7 +4304,6 @@ class TestRhelSysConfigRendering(CiTestCase):
59e311
 
59e311
     with_logs = True
59e311
 
59e311
-    nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf"
59e311
     scripts_dir = "/etc/sysconfig/network-scripts"
59e311
     header = (
59e311
         "# Created by cloud-init on instance boot automatically, "
59e311
@@ -4072,78 +4878,6 @@ USERCTL=no
59e311
         self._compare_files_to_expected(entry[self.expected_name], found)
59e311
         self._assert_headers(found)
59e311
 
59e311
-    def test_check_ifcfg_rh(self):
59e311
-        """ifcfg-rh plugin is added NetworkManager.conf if conf present."""
59e311
-        render_dir = self.tmp_dir()
59e311
-        nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file)
59e311
-        util.ensure_dir(os.path.dirname(nm_cfg))
59e311
-
59e311
-        # write a template nm.conf, note plugins is a list here
59e311
-        with open(nm_cfg, "w") as fh:
59e311
-            fh.write("# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n")
59e311
-        self.assertTrue(os.path.exists(nm_cfg))
59e311
-
59e311
-        # render and read
59e311
-        entry = NETWORK_CONFIGS["small"]
59e311
-        found = self._render_and_read(
59e311
-            network_config=yaml.load(entry["yaml"]), dir=render_dir
59e311
-        )
59e311
-        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
-        self._assert_headers(found)
59e311
-
59e311
-        # check ifcfg-rh is in the 'plugins' list
59e311
-        config = sysconfig.ConfigObj(nm_cfg)
59e311
-        self.assertIn("ifcfg-rh", config["main"]["plugins"])
59e311
-
59e311
-    def test_check_ifcfg_rh_plugins_string(self):
59e311
-        """ifcfg-rh plugin is append when plugins is a string."""
59e311
-        render_dir = self.tmp_path("render")
59e311
-        os.makedirs(render_dir)
59e311
-        nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file)
59e311
-        util.ensure_dir(os.path.dirname(nm_cfg))
59e311
-
59e311
-        # write a template nm.conf, note plugins is a value here
59e311
-        util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\nplugins=foo\n")
59e311
-
59e311
-        # render and read
59e311
-        entry = NETWORK_CONFIGS["small"]
59e311
-        found = self._render_and_read(
59e311
-            network_config=yaml.load(entry["yaml"]), dir=render_dir
59e311
-        )
59e311
-        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
-        self._assert_headers(found)
59e311
-
59e311
-        # check raw content has plugin
59e311
-        nm_file_content = util.load_file(nm_cfg)
59e311
-        self.assertIn("ifcfg-rh", nm_file_content)
59e311
-
59e311
-        # check ifcfg-rh is in the 'plugins' list
59e311
-        config = sysconfig.ConfigObj(nm_cfg)
59e311
-        self.assertIn("ifcfg-rh", config["main"]["plugins"])
59e311
-
59e311
-    def test_check_ifcfg_rh_plugins_no_plugins(self):
59e311
-        """enable_ifcfg_plugin creates plugins value if missing."""
59e311
-        render_dir = self.tmp_path("render")
59e311
-        os.makedirs(render_dir)
59e311
-        nm_cfg = subp.target_path(render_dir, path=self.nm_cfg_file)
59e311
-        util.ensure_dir(os.path.dirname(nm_cfg))
59e311
-
59e311
-        # write a template nm.conf, note plugins is missing
59e311
-        util.write_file(nm_cfg, "# test_check_ifcfg_rh\n[main]\n")
59e311
-        self.assertTrue(os.path.exists(nm_cfg))
59e311
-
59e311
-        # render and read
59e311
-        entry = NETWORK_CONFIGS["small"]
59e311
-        found = self._render_and_read(
59e311
-            network_config=yaml.load(entry["yaml"]), dir=render_dir
59e311
-        )
59e311
-        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
-        self._assert_headers(found)
59e311
-
59e311
-        # check ifcfg-rh is in the 'plugins' list
59e311
-        config = sysconfig.ConfigObj(nm_cfg)
59e311
-        self.assertIn("ifcfg-rh", config["main"]["plugins"])
59e311
-
59e311
     def test_netplan_dhcp_false_disable_dhcp_in_state(self):
59e311
         """netplan config with dhcp[46]: False should not add dhcp in state"""
59e311
         net_config = yaml.load(NETPLAN_DHCP_FALSE)
59e311
@@ -4699,6 +5433,281 @@ STARTMODE=auto
59e311
         self._assert_headers(found)
59e311
 
59e311
 
59e311
+@mock.patch(
59e311
+    "cloudinit.net.is_openvswitch_internal_interface",
59e311
+    mock.Mock(return_value=False),
59e311
+)
59e311
+class TestNetworkManagerRendering(CiTestCase):
59e311
+
59e311
+    with_logs = True
59e311
+
59e311
+    scripts_dir = "/etc/NetworkManager/system-connections"
59e311
+
59e311
+    expected_name = "expected_network_manager"
59e311
+
59e311
+    def _get_renderer(self):
59e311
+        return network_manager.Renderer()
59e311
+
59e311
+    def _render_and_read(self, network_config=None, state=None, dir=None):
59e311
+        if dir is None:
59e311
+            dir = self.tmp_dir()
59e311
+
59e311
+        if network_config:
59e311
+            ns = network_state.parse_net_config_data(network_config)
59e311
+        elif state:
59e311
+            ns = state
59e311
+        else:
59e311
+            raise ValueError("Expected data or state, got neither")
59e311
+
59e311
+        renderer = self._get_renderer()
59e311
+        renderer.render_network_state(ns, target=dir)
59e311
+        return dir2dict(dir)
59e311
+
59e311
+    def _compare_files_to_expected(self, expected, found):
59e311
+        orig_maxdiff = self.maxDiff
59e311
+        expected_d = dict(
59e311
+            (os.path.join(self.scripts_dir, k), v) for k, v in expected.items()
59e311
+        )
59e311
+
59e311
+        try:
59e311
+            self.maxDiff = None
59e311
+            self.assertEqual(expected_d, found)
59e311
+        finally:
59e311
+            self.maxDiff = orig_maxdiff
59e311
+
59e311
+    @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
59e311
+    @mock.patch("cloudinit.net.sys_dev_path")
59e311
+    @mock.patch("cloudinit.net.read_sys_net")
59e311
+    @mock.patch("cloudinit.net.get_devicelist")
59e311
+    def test_default_generation(
59e311
+        self,
59e311
+        mock_get_devicelist,
59e311
+        mock_read_sys_net,
59e311
+        mock_sys_dev_path,
59e311
+        m_get_cmdline,
59e311
+    ):
59e311
+        tmp_dir = self.tmp_dir()
59e311
+        _setup_test(
59e311
+            tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path
59e311
+        )
59e311
+
59e311
+        network_cfg = net.generate_fallback_config()
59e311
+        ns = network_state.parse_net_config_data(
59e311
+            network_cfg, skip_broken=False
59e311
+        )
59e311
+
59e311
+        render_dir = os.path.join(tmp_dir, "render")
59e311
+        os.makedirs(render_dir)
59e311
+
59e311
+        renderer = self._get_renderer()
59e311
+        renderer.render_network_state(ns, target=render_dir)
59e311
+
59e311
+        found = dir2dict(render_dir)
59e311
+        self._compare_files_to_expected(
59e311
+            {
59e311
+                "cloud-init-eth1000.nmconnection": textwrap.dedent(
59e311
+                    """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth1000
59e311
+                uuid=8c517500-0c95-5308-9c8a-3092eebc44eb
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=07:1C:C6:75:A4:BE
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+                ),
59e311
+            },
59e311
+            found,
59e311
+        )
59e311
+
59e311
+    def test_openstack_rendering_samples(self):
59e311
+        for os_sample in OS_SAMPLES:
59e311
+            render_dir = self.tmp_dir()
59e311
+            ex_input = os_sample["in_data"]
59e311
+            ex_mac_addrs = os_sample["in_macs"]
59e311
+            network_cfg = openstack.convert_net_json(
59e311
+                ex_input, known_macs=ex_mac_addrs
59e311
+            )
59e311
+            ns = network_state.parse_net_config_data(
59e311
+                network_cfg, skip_broken=False
59e311
+            )
59e311
+            renderer = self._get_renderer()
59e311
+            # render a multiple times to simulate reboots
59e311
+            renderer.render_network_state(ns, target=render_dir)
59e311
+            renderer.render_network_state(ns, target=render_dir)
59e311
+            renderer.render_network_state(ns, target=render_dir)
59e311
+            for fn, expected_content in os_sample.get(self.expected_name, []):
59e311
+                with open(os.path.join(render_dir, fn)) as fh:
59e311
+                    self.assertEqual(expected_content, fh.read())
59e311
+
59e311
+    def test_network_config_v1_samples(self):
59e311
+        ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET)
59e311
+        render_dir = self.tmp_path("render")
59e311
+        os.makedirs(render_dir)
59e311
+        renderer = self._get_renderer()
59e311
+        renderer.render_network_state(ns, target=render_dir)
59e311
+        found = dir2dict(render_dir)
59e311
+        self._compare_files_to_expected(
59e311
+            {
59e311
+                "cloud-init-interface0.nmconnection": textwrap.dedent(
59e311
+                    """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init interface0
59e311
+                uuid=8b6862ed-dbd6-5830-93f7-a91451c13828
59e311
+                type=ethernet
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+                mac-address=52:54:00:12:34:00
59e311
+
59e311
+                [ipv4]
59e311
+                method=manual
59e311
+                may-fail=false
59e311
+                address1=10.0.2.15/24
59e311
+                gateway=10.0.2.2
59e311
+
59e311
+                """
59e311
+                ),
59e311
+            },
59e311
+            found,
59e311
+        )
59e311
+
59e311
+    def test_config_with_explicit_loopback(self):
59e311
+        render_dir = self.tmp_path("render")
59e311
+        os.makedirs(render_dir)
59e311
+        ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
59e311
+        renderer = self._get_renderer()
59e311
+        renderer.render_network_state(ns, target=render_dir)
59e311
+        found = dir2dict(render_dir)
59e311
+        self._compare_files_to_expected(
59e311
+            {
59e311
+                "cloud-init-eth0.nmconnection": textwrap.dedent(
59e311
+                    """\
59e311
+                # Generated by cloud-init. Changes will be lost.
59e311
+
59e311
+                [connection]
59e311
+                id=cloud-init eth0
59e311
+                uuid=1dd9a779-d327-56e1-8454-c65e2556c12c
59e311
+                type=ethernet
59e311
+                interface-name=eth0
59e311
+
59e311
+                [user]
59e311
+                org.freedesktop.NetworkManager.origin=cloud-init
59e311
+
59e311
+                [ethernet]
59e311
+
59e311
+                [ipv4]
59e311
+                method=auto
59e311
+                may-fail=false
59e311
+
59e311
+                """
59e311
+                ),
59e311
+            },
59e311
+            found,
59e311
+        )
59e311
+
59e311
+    def test_bond_config(self):
59e311
+        entry = NETWORK_CONFIGS["bond"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_vlan_config(self):
59e311
+        entry = NETWORK_CONFIGS["vlan"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_bridge_config(self):
59e311
+        entry = NETWORK_CONFIGS["bridge"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_manual_config(self):
59e311
+        entry = NETWORK_CONFIGS["manual"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_all_config(self):
59e311
+        entry = NETWORK_CONFIGS["all"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+        self.assertNotIn(
59e311
+            "WARNING: Network config: ignoring eth0.101 device-level mtu",
59e311
+            self.logs.getvalue(),
59e311
+        )
59e311
+
59e311
+    def test_small_config(self):
59e311
+        entry = NETWORK_CONFIGS["small"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_v4_and_v6_static_config(self):
59e311
+        entry = NETWORK_CONFIGS["v4_and_v6_static"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+        expected_msg = (
59e311
+            "WARNING: Network config: ignoring iface0 device-level mtu:8999"
59e311
+            " because ipv4 subnet-level mtu:9000 provided."
59e311
+        )
59e311
+        self.assertIn(expected_msg, self.logs.getvalue())
59e311
+
59e311
+    def test_dhcpv6_only_config(self):
59e311
+        entry = NETWORK_CONFIGS["dhcpv6_only"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_simple_render_ipv6_slaac(self):
59e311
+        entry = NETWORK_CONFIGS["ipv6_slaac"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_dhcpv6_stateless_config(self):
59e311
+        entry = NETWORK_CONFIGS["dhcpv6_stateless"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_wakeonlan_disabled_config_v2(self):
59e311
+        entry = NETWORK_CONFIGS["wakeonlan_disabled"]
59e311
+        found = self._render_and_read(
59e311
+            network_config=yaml.load(entry["yaml_v2"])
59e311
+        )
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_wakeonlan_enabled_config_v2(self):
59e311
+        entry = NETWORK_CONFIGS["wakeonlan_enabled"]
59e311
+        found = self._render_and_read(
59e311
+            network_config=yaml.load(entry["yaml_v2"])
59e311
+        )
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_render_v4_and_v6(self):
59e311
+        entry = NETWORK_CONFIGS["v4_and_v6"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+    def test_render_v6_and_v4(self):
59e311
+        entry = NETWORK_CONFIGS["v6_and_v4"]
59e311
+        found = self._render_and_read(network_config=yaml.load(entry["yaml"]))
59e311
+        self._compare_files_to_expected(entry[self.expected_name], found)
59e311
+
59e311
+
59e311
+@mock.patch(
59e311
+    "cloudinit.net.is_openvswitch_internal_interface",
59e311
+    mock.Mock(return_value=False),
59e311
+)
59e311
 class TestEniNetRendering(CiTestCase):
59e311
     @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
59e311
     @mock.patch("cloudinit.net.sys_dev_path")
59e311
@@ -6136,9 +7145,9 @@ class TestNetworkdRoundTrip(CiTestCase):
59e311
 
59e311
 class TestRenderersSelect:
59e311
     @pytest.mark.parametrize(
59e311
-        "renderer_selected,netplan,eni,nm,scfg,sys,networkd",
59e311
+        "renderer_selected,netplan,eni,sys,network_manager,networkd",
59e311
         (
59e311
-            # -netplan -ifupdown -nm -scfg -sys raises error
59e311
+            # -netplan -ifupdown -sys -network-manager -networkd raises error
59e311
             (
59e311
                 net.RendererNotFoundError,
59e311
                 False,
59e311
@@ -6146,52 +7155,51 @@ class TestRenderersSelect:
59e311
                 False,
59e311
                 False,
59e311
                 False,
59e311
-                False,
59e311
             ),
59e311
-            # -netplan +ifupdown -nm -scfg -sys selects eni
59e311
-            ("eni", False, True, False, False, False, False),
59e311
-            # +netplan +ifupdown -nm -scfg -sys selects eni
59e311
-            ("eni", True, True, False, False, False, False),
59e311
-            # +netplan -ifupdown -nm -scfg -sys selects netplan
59e311
-            ("netplan", True, False, False, False, False, False),
59e311
-            # Ubuntu with Network-Manager installed
59e311
-            # +netplan -ifupdown +nm -scfg -sys selects netplan
59e311
-            ("netplan", True, False, True, False, False, False),
59e311
-            # Centos/OpenSuse with Network-Manager installed selects sysconfig
59e311
-            # -netplan -ifupdown +nm -scfg +sys selects netplan
59e311
-            ("sysconfig", False, False, True, False, True, False),
59e311
-            # -netplan -ifupdown -nm -scfg -sys +networkd selects networkd
59e311
-            ("networkd", False, False, False, False, False, True),
59e311
+            # -netplan +ifupdown -sys -nm -networkd selects eni
59e311
+            ("eni", False, True, False, False, False),
59e311
+            # +netplan +ifupdown -sys -nm -networkd selects eni
59e311
+            ("eni", True, True, False, False, False),
59e311
+            # +netplan -ifupdown -sys -nm -networkd selects netplan
59e311
+            ("netplan", True, False, False, False, False),
59e311
+            # +netplan -ifupdown -sys -nm -networkd selects netplan
59e311
+            ("netplan", True, False, False, False, False),
59e311
+            # -netplan -ifupdown +sys -nm -networkd selects sysconfig
59e311
+            ("sysconfig", False, False, True, False, False),
59e311
+            # -netplan -ifupdown +sys +nm -networkd selects sysconfig
59e311
+            ("sysconfig", False, False, True, True, False),
59e311
+            # -netplan -ifupdown -sys +nm -networkd selects nm
59e311
+            ("network-manager", False, False, False, True, False),
59e311
+            # -netplan -ifupdown -sys +nm +networkd selects nm
59e311
+            ("network-manager", False, False, False, True, True),
59e311
+            # -netplan -ifupdown -sys -nm +networkd selects networkd
59e311
+            ("networkd", False, False, False, False, True),
59e311
         ),
59e311
     )
59e311
     @mock.patch("cloudinit.net.renderers.networkd.available")
59e311
+    @mock.patch("cloudinit.net.renderers.network_manager.available")
59e311
     @mock.patch("cloudinit.net.renderers.netplan.available")
59e311
     @mock.patch("cloudinit.net.renderers.sysconfig.available")
59e311
-    @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig")
59e311
-    @mock.patch("cloudinit.net.renderers.sysconfig.available_nm")
59e311
     @mock.patch("cloudinit.net.renderers.eni.available")
59e311
     def test_valid_renderer_from_defaults_depending_on_availability(
59e311
         self,
59e311
         m_eni_avail,
59e311
-        m_nm_avail,
59e311
-        m_scfg_avail,
59e311
         m_sys_avail,
59e311
         m_netplan_avail,
59e311
+        m_network_manager_avail,
59e311
         m_networkd_avail,
59e311
         renderer_selected,
59e311
         netplan,
59e311
         eni,
59e311
-        nm,
59e311
-        scfg,
59e311
         sys,
59e311
+        network_manager,
59e311
         networkd,
59e311
     ):
59e311
         """Assert proper renderer per DEFAULT_PRIORITY given availability."""
59e311
         m_eni_avail.return_value = eni  # ifupdown pkg presence
59e311
-        m_nm_avail.return_value = nm  # network-manager presence
59e311
-        m_scfg_avail.return_value = scfg  # sysconfig presence
59e311
         m_sys_avail.return_value = sys  # sysconfig/ifup/down presence
59e311
         m_netplan_avail.return_value = netplan  # netplan presence
59e311
+        m_network_manager_avail.return_value = network_manager  # NM presence
59e311
         m_networkd_avail.return_value = networkd  # networkd presence
59e311
         if isinstance(renderer_selected, str):
59e311
             (renderer_name, _rnd_class) = renderers.select(
59e311
@@ -6249,7 +7257,7 @@ class TestNetRenderers(CiTestCase):
59e311
             priority=["sysconfig", "eni"],
59e311
         )
59e311
 
59e311
-    @mock.patch("cloudinit.net.sysconfig.available_sysconfig")
59e311
+    @mock.patch("cloudinit.net.sysconfig.available")
59e311
     @mock.patch("cloudinit.util.system_info")
59e311
     def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail):
59e311
         m_avail.return_value = True
59e311
diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py
59e311
index 3c29e2f7..4525c49c 100644
59e311
--- a/tests/unittests/test_net_activators.py
59e311
+++ b/tests/unittests/test_net_activators.py
59e311
@@ -41,18 +41,20 @@ NETPLAN_CALL_LIST = [
59e311
 
59e311
 @pytest.fixture
59e311
 def available_mocks():
59e311
-    mocks = namedtuple("Mocks", "m_which, m_file")
59e311
+    mocks = namedtuple("Mocks", "m_which, m_file, m_exists")
59e311
     with patch("cloudinit.subp.which", return_value=True) as m_which:
59e311
         with patch("os.path.isfile", return_value=True) as m_file:
59e311
-            yield mocks(m_which, m_file)
59e311
+            with patch("os.path.exists", return_value=True) as m_exists:
59e311
+                yield mocks(m_which, m_file, m_exists)
59e311
 
59e311
 
59e311
 @pytest.fixture
59e311
 def unavailable_mocks():
59e311
-    mocks = namedtuple("Mocks", "m_which, m_file")
59e311
+    mocks = namedtuple("Mocks", "m_which, m_file, m_exists")
59e311
     with patch("cloudinit.subp.which", return_value=False) as m_which:
59e311
         with patch("os.path.isfile", return_value=False) as m_file:
59e311
-            yield mocks(m_which, m_file)
59e311
+            with patch("os.path.exists", return_value=False) as m_exists:
59e311
+                yield mocks(m_which, m_file, m_exists)
59e311
 
59e311
 
59e311
 class TestSearchAndSelect:
59e311
@@ -113,10 +115,6 @@ NETPLAN_AVAILABLE_CALLS = [
59e311
     (("netplan",), {"search": ["/usr/sbin", "/sbin"], "target": None}),
59e311
 ]
59e311
 
59e311
-NETWORK_MANAGER_AVAILABLE_CALLS = [
59e311
-    (("nmcli",), {"target": None}),
59e311
-]
59e311
-
59e311
 NETWORKD_AVAILABLE_CALLS = [
59e311
     (("ip",), {"search": ["/usr/sbin", "/bin"], "target": None}),
59e311
     (("systemctl",), {"search": ["/usr/sbin", "/bin"], "target": None}),
59e311
@@ -128,7 +126,6 @@ NETWORKD_AVAILABLE_CALLS = [
59e311
     [
59e311
         (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS),
59e311
         (NetplanActivator, NETPLAN_AVAILABLE_CALLS),
59e311
-        (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS),
59e311
         (NetworkdActivator, NETWORKD_AVAILABLE_CALLS),
59e311
     ],
59e311
 )
59e311
@@ -144,8 +141,72 @@ IF_UP_DOWN_BRING_UP_CALL_LIST = [
59e311
 ]
59e311
 
59e311
 NETWORK_MANAGER_BRING_UP_CALL_LIST = [
59e311
-    ((["nmcli", "connection", "up", "ifname", "eth0"],), {}),
59e311
-    ((["nmcli", "connection", "up", "ifname", "eth1"],), {}),
59e311
+    (
59e311
+        (
59e311
+            [
59e311
+                "nmcli",
59e311
+                "connection",
59e311
+                "load",
59e311
+                "".join(
59e311
+                    [
59e311
+                        "/etc/NetworkManager/system-connections",
59e311
+                        "/cloud-init-eth0.nmconnection",
59e311
+                    ]
59e311
+                ),
59e311
+            ],
59e311
+        ),
59e311
+        {},
59e311
+    ),
59e311
+    (
59e311
+        (
59e311
+            [
59e311
+                "nmcli",
59e311
+                "connection",
59e311
+                "up",
59e311
+                "filename",
59e311
+                "".join(
59e311
+                    [
59e311
+                        "/etc/NetworkManager/system-connections",
59e311
+                        "/cloud-init-eth0.nmconnection",
59e311
+                    ]
59e311
+                ),
59e311
+            ],
59e311
+        ),
59e311
+        {},
59e311
+    ),
59e311
+    (
59e311
+        (
59e311
+            [
59e311
+                "nmcli",
59e311
+                "connection",
59e311
+                "load",
59e311
+                "".join(
59e311
+                    [
59e311
+                        "/etc/NetworkManager/system-connections",
59e311
+                        "/cloud-init-eth1.nmconnection",
59e311
+                    ]
59e311
+                ),
59e311
+            ],
59e311
+        ),
59e311
+        {},
59e311
+    ),
59e311
+    (
59e311
+        (
59e311
+            [
59e311
+                "nmcli",
59e311
+                "connection",
59e311
+                "up",
59e311
+                "filename",
59e311
+                "".join(
59e311
+                    [
59e311
+                        "/etc/NetworkManager/system-connections",
59e311
+                        "/cloud-init-eth1.nmconnection",
59e311
+                    ]
59e311
+                ),
59e311
+            ],
59e311
+        ),
59e311
+        {},
59e311
+    ),
59e311
 ]
59e311
 
59e311
 NETWORKD_BRING_UP_CALL_LIST = [
59e311
@@ -169,9 +230,11 @@ class TestActivatorsBringUp:
59e311
     def test_bring_up_interface(
59e311
         self, m_subp, activator, expected_call_list, available_mocks
59e311
     ):
59e311
+        index = 0
59e311
         activator.bring_up_interface("eth0")
59e311
-        assert len(m_subp.call_args_list) == 1
59e311
-        assert m_subp.call_args_list[0] == expected_call_list[0]
59e311
+        for call in m_subp.call_args_list:
59e311
+            assert call == expected_call_list[index]
59e311
+            index += 1
59e311
 
59e311
     @patch("cloudinit.subp.subp", return_value=("", ""))
59e311
     def test_bring_up_interfaces(
59e311
@@ -208,8 +271,8 @@ IF_UP_DOWN_BRING_DOWN_CALL_LIST = [
59e311
 ]
59e311
 
59e311
 NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [
59e311
-    ((["nmcli", "connection", "down", "eth0"],), {}),
59e311
-    ((["nmcli", "connection", "down", "eth1"],), {}),
59e311
+    ((["nmcli", "device", "disconnect", "eth0"],), {}),
59e311
+    ((["nmcli", "device", "disconnect", "eth1"],), {}),
59e311
 ]
59e311
 
59e311
 NETWORKD_BRING_DOWN_CALL_LIST = [
59e311
-- 
59e311
2.31.1
59e311