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