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