Blob Blame History Raw
diff -Nur nmstate-1.3.3.old/libnmstate/dns.py nmstate-1.3.3/libnmstate/dns.py
--- nmstate-1.3.3.old/libnmstate/dns.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/dns.py	2023-02-15 15:48:33.668708842 +0800
@@ -144,6 +144,7 @@
         Find interface to store the DNS configurations in the order of:
             * Any interface with static gateway
             * Any interface configured as dynamic IP with 'auto-dns:False'
+        The loopback interface is ignored.
         Return tuple: (ipv4_iface, ipv6_iface)
         """
         ipv4_iface, ipv6_iface = self._find_ifaces_with_static_gateways(
@@ -168,6 +169,8 @@
         ipv4_iface = None
         ipv6_iface = None
         for iface_name, route_set in route_state.config_iface_routes.items():
+            if iface_name == "lo":
+                continue
             for route in route_set:
                 if ipv4_iface and ipv6_iface:
                     return (ipv4_iface, ipv6_iface)
diff -Nur nmstate-1.3.3.old/libnmstate/ifaces/base_iface.py nmstate-1.3.3/libnmstate/ifaces/base_iface.py
--- nmstate-1.3.3.old/libnmstate/ifaces/base_iface.py	2023-02-15 15:48:11.745846015 +0800
+++ nmstate-1.3.3/libnmstate/ifaces/base_iface.py	2023-02-15 15:48:45.118768209 +0800
@@ -61,6 +61,7 @@
                 InterfaceIP.AUTO_ROUTES,
                 InterfaceIP.AUTO_GATEWAY,
                 InterfaceIP.AUTO_DNS,
+                InterfaceIP.AUTO_ROUTE_METRIC,
             ):
                 self._info.pop(dhcp_option, None)
 
@@ -123,7 +124,6 @@
 
 
 class BaseIface:
-    CONTROLLER_METADATA = "_controller"
     CONTROLLER_TYPE_METADATA = "_controller_type"
     DNS_METADATA = "_dns"
     ROUTES_METADATA = "_routes"
@@ -253,7 +253,6 @@
                     self.ip_state(family).validate(
                         IPState(family, self._origin_info.get(family, {}))
                     )
-                self._validate_port_ip()
                 ip_state = self.ip_state(family)
                 ip_state.remove_link_local_address()
                 self._info[family] = ip_state.to_dict()
@@ -302,7 +301,7 @@
             if current_external_ids:
                 other._info[OvsDB.OVS_DB_SUBTREE].pop(OvsDB.EXTERNAL_IDS)
 
-    def _validate_port_ip(self):
+    def validate_port_ip(self):
         for family in (Interface.IPV4, Interface.IPV6):
             ip_state = IPState(family, self._origin_info.get(family, {}))
             if (
@@ -356,10 +355,11 @@
         return False
 
     def set_controller(self, controller_iface_name, controller_type):
-        self._info[BaseIface.CONTROLLER_METADATA] = controller_iface_name
+        self._info[Interface.CONTROLLER] = controller_iface_name
         self._info[BaseIface.CONTROLLER_TYPE_METADATA] = controller_type
         if (
-            not self.can_have_ip_as_port
+            controller_iface_name
+            and not self.can_have_ip_as_port
             and controller_type != InterfaceType.VRF
         ):
             for family in (Interface.IPV4, Interface.IPV6):
@@ -367,7 +367,7 @@
 
     @property
     def controller(self):
-        return self._info.get(BaseIface.CONTROLLER_METADATA)
+        return self._info.get(Interface.CONTROLLER)
 
     @property
     def controller_type(self):
@@ -378,7 +378,8 @@
             for port_name in self.port:
                 port_iface = ifaces.all_kernel_ifaces.get(port_name)
                 if port_iface:
-                    port_iface.set_controller(self.name, self.type)
+                    if port_iface.controller != "":
+                        port_iface.set_controller(self.name, self.type)
 
     def update(self, info):
         self._info.update(info)
@@ -444,6 +445,8 @@
             state[Interface.STATE] = InterfaceState.DOWN
         _convert_ovs_external_ids_values_to_string(state)
         state.pop(BaseIface.PERMANENT_MAC_ADDRESS_METADATA, None)
+        if self.controller == "":
+            state.pop(Interface.CONTROLLER, None)
 
         return state
 
diff -Nur nmstate-1.3.3.old/libnmstate/ifaces/ethtool.py nmstate-1.3.3/libnmstate/ifaces/ethtool.py
--- nmstate-1.3.3.old/libnmstate/ifaces/ethtool.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/ifaces/ethtool.py	2023-02-15 15:48:33.672042194 +0800
@@ -176,6 +176,7 @@
         "rx-ntuple-filter",
         "rx-vlan-hw-parse",
         "tx-vlan-hw-insert",
+        "highdma",
     }
 
     def __init__(self, feature_info):
diff -Nur nmstate-1.3.3.old/libnmstate/ifaces/ifaces.py nmstate-1.3.3/libnmstate/ifaces/ifaces.py
--- nmstate-1.3.3.old/libnmstate/ifaces/ifaces.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/ifaces/ifaces.py	2023-02-15 15:48:45.118768209 +0800
@@ -1,22 +1,6 @@
-#
-# Copyright (c) 2020-2021 Red Hat, Inc.
-#
-# This file is part of nmstate
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 2.1 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
-#
+# SPDX-License-Identifier: LGPL-2.1-or-later
 
+from copy import deepcopy
 import logging
 
 from libnmstate.error import NmstateKernelIntegerRoundedError
@@ -24,6 +8,7 @@
 from libnmstate.error import NmstateVerificationError
 from libnmstate.prettystate import format_desired_current_state_diff
 from libnmstate.schema import BondMode
+from libnmstate.schema import Ethernet
 from libnmstate.schema import Interface
 from libnmstate.schema import InterfaceType
 from libnmstate.schema import InterfaceState
@@ -97,18 +82,20 @@
         self._cur_user_space_ifaces = _UserSpaceIfaces()
         if cur_iface_infos:
             for iface_info in cur_iface_infos:
-                cur_iface = _to_specific_iface_obj(iface_info, save_to_disk)
+                cur_iface = _to_specific_iface_obj(
+                    deepcopy(iface_info), save_to_disk
+                )
                 if cur_iface.is_user_space_only:
-                    self._user_space_ifaces.set(cur_iface)
+                    self._user_space_ifaces.set(deepcopy(cur_iface))
                     self._cur_user_space_ifaces.set(cur_iface)
                 else:
-                    self._kernel_ifaces[cur_iface.name] = cur_iface
+                    self._kernel_ifaces[cur_iface.name] = deepcopy(cur_iface)
                     self._cur_kernel_ifaces[cur_iface.name] = cur_iface
 
         if des_iface_infos:
             for iface_info in des_iface_infos:
                 iface = BaseIface(iface_info, save_to_disk)
-                if not iface.is_up and self._gen_conf_mode:
+                if not (iface.is_up or iface.is_down) and self._gen_conf_mode:
                     continue
                 if iface.type == InterfaceType.UNKNOWN:
                     cur_ifaces = self._get_cur_ifaces(iface.name)
@@ -157,6 +144,7 @@
             self._validate_infiniband_as_bridge_port()
             self._validate_infiniband_as_bond_port()
             self._apply_copy_mac_from()
+            self._validate_controller_and_port_list_conflict()
             self.gen_metadata()
             for iface in self.all_ifaces():
                 if iface.is_desired and iface.is_up:
@@ -164,6 +152,67 @@
 
             self._pre_edit_validation_and_cleanup()
 
+    # Return True when SR-IOV `total-vfs` changed and having interface not
+    # exists in current.
+    def has_vf_count_change_and_missing_eth(self):
+        return self._has_vf_count_change() and self._has_missing_veth()
+
+    def _has_vf_count_change(self):
+        for iface in self.all_kernel_ifaces.values():
+            cur_iface = self._cur_kernel_ifaces.get(iface.name)
+            if (
+                cur_iface
+                and iface.is_desired
+                and iface.is_up
+                and iface.type == InterfaceType.ETHERNET
+            ):
+                des_vf_count = (
+                    iface.original_desire_dict.get(Ethernet.CONFIG_SUBTREE, {})
+                    .get(Ethernet.SRIOV_SUBTREE, {})
+                    .get(Ethernet.SRIOV.TOTAL_VFS, 0)
+                )
+                cur_vf_count = (
+                    cur_iface.raw.get(Ethernet.CONFIG_SUBTREE, {})
+                    .get(Ethernet.SRIOV_SUBTREE, {})
+                    .get(Ethernet.SRIOV.TOTAL_VFS, 0)
+                )
+                if des_vf_count != cur_vf_count:
+                    return True
+        return False
+
+    def _has_missing_veth(self):
+        for iface in self.all_kernel_ifaces.values():
+            cur_iface = self._cur_kernel_ifaces.get(iface.name)
+            if cur_iface is None and iface.type == InterfaceType.ETHERNET:
+                return True
+        return False
+
+    # Return list of cloned iface_info(dictionary) which SRIOV PF conf only.
+    def get_sriov_pf_ifaces(self):
+        sriov_ifaces = []
+        for iface in self.all_kernel_ifaces.values():
+            if (
+                iface.is_desired
+                and iface.is_up
+                and iface.type == InterfaceType.ETHERNET
+            ):
+                sriov_conf = iface.original_desire_dict.get(
+                    Ethernet.CONFIG_SUBTREE, {}
+                ).get(Ethernet.SRIOV_SUBTREE, {})
+                if sriov_conf:
+                    eth_conf = iface.original_desire_dict.get(
+                        Ethernet.CONFIG_SUBTREE
+                    )
+                    sriov_ifaces.append(
+                        {
+                            Interface.NAME: iface.name,
+                            Interface.TYPE: InterfaceType.ETHERNET,
+                            Interface.STATE: InterfaceState.UP,
+                            Ethernet.CONFIG_SUBTREE: deepcopy(eth_conf),
+                        }
+                    )
+        return sriov_ifaces
+
     @property
     def _ignored_ifaces(self):
         return [iface for iface in self.all_ifaces() if iface.is_ignore]
@@ -275,6 +324,13 @@
         self._validate_ovs_patch_peers()
         self._remove_unknown_type_interfaces()
         self._validate_veth_peers()
+        self._resolve_controller_type()
+        self._validate_port_ip()
+
+    def _validate_port_ip(self):
+        for iface in self.all_ifaces():
+            if iface.is_desired and iface.is_up:
+                iface.validate_port_ip()
 
     def _bring_port_up_if_not_in_desire(self):
         """
@@ -400,6 +456,72 @@
                             f"{iface.name} is in {iface.bond_mode} mode."
                         )
 
+    def _validate_controller_and_port_list_conflict(self):
+        """
+        Validate Check whether user defined both controller property and port
+        list of controller interface, examples of invalid desire state:
+            * eth1 has controller: br1, but br1 has no eth1 in port list
+            * eth2 has controller: br1, but br2 has eth2 in port list
+            * eth1 has controller: Some("") (detach), but br1 has eth1 in port
+              list
+        """
+        self._validate_controller_not_in_port_list()
+        self._validate_controller_in_other_port_list()
+
+    def _validate_controller_not_in_port_list(self):
+        for iface_name, iface in self._kernel_ifaces.items():
+            if (
+                not iface.is_up
+                or not iface.controller
+                or Interface.CONTROLLER not in iface.original_desire_dict
+            ):
+                continue
+            ctrl_iface = self._user_space_ifaces.get(
+                iface.controller, InterfaceType.OVS_BRIDGE
+            )
+            if not ctrl_iface:
+                ctrl_iface = self._kernel_ifaces.get(iface.controller)
+            if ctrl_iface:
+                if not ctrl_iface.is_desired:
+                    continue
+                if ctrl_iface.port and iface_name not in ctrl_iface.port:
+                    raise NmstateValueError(
+                        f"Interface {iface_name} desired controller "
+                        f"is {iface.controller}, but not listed in port "
+                        "list of controller interface"
+                    )
+
+    def _validate_controller_in_other_port_list(self):
+        port_to_ctrl = {}
+        for iface in self.all_ifaces():
+            if iface.is_controller and iface.is_desired and iface.is_up:
+                for port in iface.port:
+                    port_to_ctrl[port] = iface.name
+
+        for iface in self._kernel_ifaces.values():
+            if (
+                not iface.is_desired
+                or not iface.is_up
+                or iface.controller is None
+                or iface.name not in port_to_ctrl
+                or Interface.CONTROLLER not in iface.original_desire_dict
+            ):
+                continue
+            ctrl_name = port_to_ctrl.get(iface.name)
+            if ctrl_name != iface.controller:
+                if iface.controller:
+                    raise NmstateValueError(
+                        f"Interface {iface.name} has controller property set "
+                        f"to {iface.controller}, but been listed as "
+                        f"port of controller {ctrl_name} "
+                    )
+                else:
+                    raise NmstateValueError(
+                        f"Interface {iface.name} desired to detach controller "
+                        "via controller property set to '', but "
+                        f"still been listed as port of controller {ctrl_name}"
+                    )
+
     def _handle_controller_port_list_change(self):
         """
         * Mark port interface as changed if controller removed.
@@ -419,6 +541,10 @@
                 changed_port = (des_port | cur_port) - (des_port & cur_port)
                 for iface_name in changed_port:
                     self._kernel_ifaces[iface_name].mark_as_changed()
+                    if iface_name not in des_port:
+                        self._kernel_ifaces[iface_name].set_controller(
+                            None, None
+                        )
             if cur_iface:
                 for port_name in iface.config_changed_port(cur_iface):
                     if port_name in self._kernel_ifaces:
@@ -823,6 +949,24 @@
                     if port_name in ignored_kernel_iface_names:
                         iface.remove_port(port_name)
 
+    def _resolve_controller_type(self):
+        for iface in self._kernel_ifaces.values():
+            if (
+                iface.is_up
+                and iface.is_desired
+                and Interface.CONTROLLER in iface.original_desire_dict
+                and iface.controller
+                and iface.controller_type is None
+            ):
+                ctrl_iface = self._cur_user_space_ifaces.get(
+                    iface.controller, InterfaceType.OVS_BRIDGE
+                )
+                if ctrl_iface is None:
+                    ctrl_iface = self._cur_kernel_ifaces.get(iface.controller)
+
+                if ctrl_iface:
+                    iface.set_controller(iface.controller, ctrl_iface.type)
+
 
 def _to_specific_iface_obj(info, save_to_disk):
     iface_type = info.get(Interface.TYPE, InterfaceType.UNKNOWN)
diff -Nur nmstate-1.3.3.old/libnmstate/ifaces/linux_bridge.py nmstate-1.3.3/libnmstate/ifaces/linux_bridge.py
--- nmstate-1.3.3.old/libnmstate/ifaces/linux_bridge.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/ifaces/linux_bridge.py	2023-02-15 15:48:33.672042194 +0800
@@ -192,9 +192,9 @@
                 # There is no good way to detect kernel HZ in user space. Hence
                 # we check whether certain value is rounded.
                 if cur_value != value:
-                    if value >= 8 * (10 ** 6):
+                    if value >= 8 * (10**6):
                         if abs(value - cur_value) <= int(
-                            value / 8 * (10 ** 6)
+                            value / 8 * (10**6)
                         ):
                             return key, value, cur_value
                     else:
diff -Nur nmstate-1.3.3.old/libnmstate/netapplier.py nmstate-1.3.3/libnmstate/netapplier.py
--- nmstate-1.3.3.old/libnmstate/netapplier.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/netapplier.py	2023-02-15 15:48:45.118768209 +0800
@@ -24,7 +24,7 @@
 
 from libnmstate import validator
 from libnmstate.error import NmstateVerificationError
-from libnmstate.schema import InterfaceType
+from libnmstate.schema import Interface
 
 from .net_state import NetState
 from .nmstate import create_checkpoints
@@ -73,20 +73,56 @@
 
     desired_state = copy.deepcopy(desired_state)
     remove_the_reserved_secrets(desired_state)
+
     with plugin_context() as plugins:
         validator.schema_validate(desired_state)
         current_state = show_with_plugins(
             plugins, include_status_data=True, include_secrets=True
         )
         validator.validate_capabilities(
-            desired_state, plugins_capabilities(plugins)
+            copy.deepcopy(desired_state), plugins_capabilities(plugins)
         )
         ignored_ifnames = _get_ignored_interface_names(plugins)
         net_state = NetState(
             desired_state, ignored_ifnames, current_state, save_to_disk
         )
         checkpoints = create_checkpoints(plugins, rollback_timeout)
-        _apply_ifaces_state(plugins, net_state, verify_change, save_to_disk)
+        # When we have VF count changes and missing eth, it might be user
+        # referring future VF in the same desire state, we just apply
+        # VF changes state only first.
+        if net_state.ifaces.has_vf_count_change_and_missing_eth():
+            sriov_ifaces = net_state.ifaces.get_sriov_pf_ifaces()
+            if sriov_ifaces:
+                pf_net_state = NetState(
+                    {Interface.KEY: sriov_ifaces},
+                    ignored_ifnames,
+                    current_state,
+                    save_to_disk,
+                )
+                _apply_ifaces_state(
+                    plugins,
+                    pf_net_state,
+                    verify_change,
+                    save_to_disk,
+                    has_sriov_pf=True,
+                )
+                # Refresh the current state
+                current_state = show_with_plugins(
+                    plugins, include_status_data=True, include_secrets=True
+                )
+                validator.validate_capabilities(
+                    desired_state, plugins_capabilities(plugins)
+                )
+                ignored_ifnames = _get_ignored_interface_names(plugins)
+                net_state = NetState(
+                    copy.deepcopy(desired_state),
+                    ignored_ifnames,
+                    current_state,
+                    save_to_disk,
+                )
+        _apply_ifaces_state(
+            plugins, net_state, verify_change, save_to_disk, has_sriov_pf=False
+        )
         if commit:
             destroy_checkpoints(plugins, checkpoints)
         else:
@@ -117,13 +153,17 @@
         rollback_checkpoints(plugins, checkpoint)
 
 
-def _apply_ifaces_state(plugins, net_state, verify_change, save_to_disk):
+def _apply_ifaces_state(
+    plugins, net_state, verify_change, save_to_disk, has_sriov_pf=False
+):
     for plugin in plugins:
-        plugin.apply_changes(net_state, save_to_disk)
+        # Do not allow plugin to modify the net_state for future verification
+        tmp_net_state = copy.deepcopy(net_state)
+        plugin.apply_changes(tmp_net_state, save_to_disk)
 
     verified = False
     if verify_change:
-        if _net_state_contains_sriov_interface(net_state):
+        if has_sriov_pf:
             # If SR-IOV is present, the verification timeout is being increased
             # to avoid timeouts due to slow drivers like i40e.
             verify_retry = VERIFY_RETRY_COUNT_SRIOV
@@ -140,14 +180,6 @@
             _verify_change(plugins, net_state)
 
 
-def _net_state_contains_sriov_interface(net_state):
-    for iface in net_state.ifaces.all_kernel_ifaces.values():
-        if iface.type == InterfaceType.ETHERNET and iface.is_sriov:
-            return True
-
-    return False
-
-
 def _verify_change(plugins, net_state):
     current_state = remove_metadata_leftover(
         show_with_plugins(plugins, include_secrets=True)
diff -Nur nmstate-1.3.3.old/libnmstate/netinfo.py nmstate-1.3.3/libnmstate/netinfo.py
--- nmstate-1.3.3.old/libnmstate/netinfo.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/netinfo.py	2023-02-15 15:48:33.672042194 +0800
@@ -39,6 +39,7 @@
                 plugins,
                 include_status_data=include_status_data,
                 include_secrets=include_secrets,
+                include_controller_prop=False,
             )
         )
 
diff -Nur nmstate-1.3.3.old/libnmstate/nispor/base_iface.py nmstate-1.3.3/libnmstate/nispor/base_iface.py
--- nmstate-1.3.3.old/libnmstate/nispor/base_iface.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nispor/base_iface.py	2023-02-15 15:48:33.672042194 +0800
@@ -115,6 +115,8 @@
             if ethtool_info_dict:
                 iface_info[Ethtool.CONFIG_SUBTREE] = ethtool_info_dict
 
+        if self.np_iface.controller:
+            iface_info[Interface.CONTROLLER] = self.np_iface.controller
         return iface_info
 
 
@@ -219,6 +221,7 @@
         "rx-ntuple-filter",
         "rx-vlan-hw-parse",
         "tx-vlan-hw-insert",
+        "highdma",
     ]
 
     def __init__(self, np_ethtool):
diff -Nur nmstate-1.3.3.old/libnmstate/nm/connection.py nmstate-1.3.3/libnmstate/nm/connection.py
--- nmstate-1.3.3.old/libnmstate/nm/connection.py	2023-02-15 15:48:11.739845988 +0800
+++ nmstate-1.3.3/libnmstate/nm/connection.py	2023-02-15 15:48:33.672042194 +0800
@@ -94,7 +94,7 @@
         self._setting = new
 
     def set_controller(self, controller, port_type):
-        if controller is not None:
+        if controller:
             self._setting.props.master = controller
             self._setting.props.slave_type = port_type
 
@@ -186,7 +186,7 @@
         settings.extend(create_ovs_interface_setting(patch_state, dpdk_state))
     elif iface.type == InterfaceType.INFINIBAND:
         ib_setting = create_infiniband_setting(
-            iface_info,
+            iface,
             nm_profile,
             iface.original_desire_dict,
         )
diff -Nur nmstate-1.3.3.old/libnmstate/nm/device.py nmstate-1.3.3/libnmstate/nm/device.py
--- nmstate-1.3.3.old/libnmstate/nm/device.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/device.py	2023-02-15 15:48:45.122101559 +0800
@@ -106,39 +106,41 @@
         self._iface_name = iface_name
         self._iface_type = iface_type
         self._nm_dev = nm_dev
+        self._action = None
 
     def run(self):
-        action = f"Delete device: {self._iface_type} {self._iface_name}"
-        user_data = action
-        self._ctx.register_async(action)
+        self._action = f"Delete device: {self._iface_type} {self._iface_name}"
+        retried = False
+        self._ctx.register_async(self._action)
         self._nm_dev.delete_async(
-            self._ctx.cancellable, self._delete_device_callback, user_data
+            self._ctx.cancellable, self._delete_device_callback, retried
         )
 
-    def _delete_device_callback(self, nm_dev, result, user_data):
-        action = user_data
+    def _delete_device_callback(self, nm_dev, result, retried):
         if self._ctx.is_cancelled():
             return
-        error = None
         try:
             nm_dev.delete_finish(result)
+            self._ctx.finish_async(self._action)
         except Exception as e:
-            error = e
-
-        if not nm_dev.is_real():
-            logging.debug(
-                f"Interface is deleted and not real/exist anymore: "
-                f"iface={self._iface_name} type={self._iface_type}"
-            )
-            if error:
-                logging.debug(f"Ignored error: {error}")
-            self._ctx.finish_async(action)
-        else:
-            self._ctx.fail(
-                NmstateLibnmError(
-                    f"{action} failed: error={error or 'unknown'}"
+            if not nm_dev.is_real():
+                logging.debug(
+                    f"Interface is deleted and not real/exist anymore: "
+                    f"iface={self._iface_name} type={self._iface_type}"
+                )
+                logging.debug(f"Ignored error: {e}")
+                self._ctx.finish_async(self._action)
+            elif retried:
+                self._ctx.fail(
+                    NmstateLibnmError(f"{self._action} failed: error={e}")
+                )
+            else:
+                retried = True
+                self._nm_dev.delete_async(
+                    self._ctx.cancellable,
+                    self._delete_device_callback,
+                    retried,
                 )
-            )
 
 
 def list_devices(client):
diff -Nur nmstate-1.3.3.old/libnmstate/nm/infiniband.py nmstate-1.3.3/libnmstate/nm/infiniband.py
--- nmstate-1.3.3.old/libnmstate/nm/infiniband.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/infiniband.py	2023-02-14 18:48:57.083997920 +0800
@@ -71,7 +71,9 @@
         return None
 
 
-def create_setting(iface_info, base_con_profile, original_iface_info):
+def create_setting(iface, base_con_profile, original_iface_info):
+    iface.pre_edit_validation_and_cleanup()
+    iface_info = iface.to_dict()
     ib_config = iface_info.get(InfiniBand.CONFIG_SUBTREE)
     if not ib_config:
         return None
diff -Nur nmstate-1.3.3.old/libnmstate/nm/ipv4.py nmstate-1.3.3/libnmstate/nm/ipv4.py
--- nmstate-1.3.3.old/libnmstate/nm/ipv4.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/ipv4.py	2023-02-15 15:48:33.672042194 +0800
@@ -27,7 +27,7 @@
 from ..ifaces import BaseIface
 from .common import NM
 
-INT32_MAX = 2 ** 31 - 1
+INT32_MAX = 2**31 - 1
 
 
 def create_setting(config, base_con_profile):
@@ -77,6 +77,9 @@
             # when the DHCP timeout expired, set it to the maximum value to
             # make this unlikely.
             setting_ipv4.props.dhcp_timeout = INT32_MAX
+            route_metric = config.get(InterfaceIPv4.AUTO_ROUTE_METRIC)
+            if route_metric is not None:
+                setting_ipv4.props.route_metric = route_metric
         elif config.get(InterfaceIPv4.ADDRESS):
             setting_ipv4.props.method = NM.SETTING_IP4_CONFIG_METHOD_MANUAL
             _add_addresses(setting_ipv4, config[InterfaceIPv4.ADDRESS])
@@ -129,6 +132,8 @@
                 info[InterfaceIPv4.AUTO_GATEWAY] = not props.never_default
                 info[InterfaceIPv4.AUTO_DNS] = not props.ignore_auto_dns
                 info[InterfaceIPv4.AUTO_ROUTE_TABLE_ID] = props.route_table
+                if props.route_metric >= 0:
+                    info[InterfaceIPv4.AUTO_ROUTE_METRIC] = props.route_metric
                 if props.dhcp_client_id:
                     info[InterfaceIPv4.DHCP_CLIENT_ID] = props.dhcp_client_id
 
diff -Nur nmstate-1.3.3.old/libnmstate/nm/ipv6.py nmstate-1.3.3/libnmstate/nm/ipv6.py
--- nmstate-1.3.3.old/libnmstate/nm/ipv6.py	2023-02-15 15:48:11.737845979 +0800
+++ nmstate-1.3.3/libnmstate/nm/ipv6.py	2023-02-15 15:50:14.713397220 +0800
@@ -31,7 +31,7 @@
 from .common import NM
 
 IPV6_DEFAULT_ROUTE_METRIC = 1024
-INT32_MAX = 2 ** 31 - 1
+INT32_MAX = 2**31 - 1
 
 
 def get_info(active_connection, applied_config):
@@ -73,6 +73,8 @@
             info[InterfaceIPv6.AUTO_ROUTE_TABLE_ID] = props.route_table
             if props.dhcp_duid:
                 info[InterfaceIPv6.DHCP_DUID] = props.dhcp_duid
+            if props.route_metric > 0:
+                info[InterfaceIPv6.AUTO_ROUTE_METRIC] = props.route_metric
             info[InterfaceIPv6.ADDR_GEN_MODE] = (
                 InterfaceIPv6.ADDR_GEN_MODE_STABLE_PRIVACY
                 if props.addr_gen_mode
@@ -146,6 +148,10 @@
         if route_table:
             setting_ip.props.route_table = route_table
 
+        route_metric = config.get(InterfaceIPv6.AUTO_ROUTE_METRIC)
+        if route_metric is not None:
+            setting_ip.props.route_metric = route_metric
+
     elif ip_addresses:
         _set_static(setting_ip, ip_addresses)
     else:
diff -Nur nmstate-1.3.3.old/libnmstate/nm/ovs.py nmstate-1.3.3/libnmstate/nm/ovs.py
--- nmstate-1.3.3.old/libnmstate/nm/ovs.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/ovs.py	2023-02-15 15:48:33.672042194 +0800
@@ -22,6 +22,7 @@
 
 from libnmstate.ifaces import ovs
 from libnmstate.ifaces.bridge import BridgeIface
+from libnmstate.ifaces.ovs import OvsBridgeIface
 from libnmstate.ifaces.ovs import OvsPortIface
 from libnmstate.schema import Interface
 from libnmstate.schema import InterfaceType
@@ -33,7 +34,6 @@
 
 
 CONTROLLER_TYPE_METADATA = "_controller_type"
-CONTROLLER_METADATA = "_controller"
 SETTING_OVS_EXTERNALIDS = "SettingOvsExternalIDs"
 SETTING_OVS_EXTERNAL_IDS_SETTING_NAME = "ovs-external-ids"
 
@@ -338,17 +338,24 @@
     iface_name = iface.name
     iface_info = iface.to_dict()
     port_options = iface_info.get(BridgeIface.BRPORT_OPTIONS_METADATA)
-    if ovs.is_ovs_lag_port(port_options):
-        port_name = port_options[OB.Port.NAME]
+    if port_options:
+        if ovs.is_ovs_lag_port(port_options):
+            port_name = port_options[OB.Port.NAME]
+        else:
+            port_name = iface_name
     else:
+        # User is attaching system port to OVS bridge via `controller` property
+        # with OVS bridge not mentioned in desired state
         port_name = iface_name
+        port_options = {}
+
     return OvsPortIface(
         {
             Interface.NAME: port_name,
             Interface.TYPE: InterfaceType.OVS_PORT,
             Interface.STATE: iface.state,
             OB.OPTIONS_SUBTREE: port_options,
-            CONTROLLER_METADATA: iface_info[CONTROLLER_METADATA],
+            Interface.CONTROLLER: iface_info[Interface.CONTROLLER],
             CONTROLLER_TYPE_METADATA: iface_info[CONTROLLER_TYPE_METADATA],
         }
     )
@@ -356,3 +363,17 @@
 
 def _is_nm_support_ovs_external_ids():
     return hasattr(NM, SETTING_OVS_EXTERNALIDS)
+
+
+def set_ovs_iface_controller_info(iface_infos):
+    pending_changes = {}
+    for iface_info in iface_infos:
+        if iface_info.get(Interface.TYPE) == InterfaceType.OVS_BRIDGE:
+            iface = OvsBridgeIface(info=iface_info, save_to_disk=True)
+            for port in iface.port:
+                pending_changes[port] = iface.name
+
+    for iface_info in iface_infos:
+        ctrl_name = pending_changes.get(iface_info[Interface.NAME])
+        if ctrl_name:
+            iface_info[Interface.CONTROLLER] = ctrl_name
diff -Nur nmstate-1.3.3.old/libnmstate/nm/plugin.py nmstate-1.3.3/libnmstate/nm/plugin.py
--- nmstate-1.3.3.old/libnmstate/nm/plugin.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/plugin.py	2023-02-15 15:48:33.672042194 +0800
@@ -49,6 +49,7 @@
 from .ovs import get_ovs_bridge_info
 from .ovs import get_ovsdb_external_ids
 from .ovs import has_ovs_capability
+from .ovs import set_ovs_iface_controller_info
 from .profiles import NmProfiles
 from .profiles import get_all_applied_configs
 from .team import get_info as get_team_info
@@ -192,6 +193,8 @@
 
             info.append(iface_info)
 
+        set_ovs_iface_controller_info(info)
+
         info.sort(key=itemgetter("name"))
 
         return info
@@ -293,7 +296,7 @@
     def generate_configurations(self, net_state):
         if not hasattr(NM, "keyfile_write"):
             raise NmstateNotSupportedError(
-                f"Current NetworkManager version does not support generating "
+                "Current NetworkManager version does not support generating "
                 "configurations, please upgrade to 1.30 or later versoin."
             )
         return NmProfiles(None).generate_config_strings(net_state)
diff -Nur nmstate-1.3.3.old/libnmstate/nm/profile.py nmstate-1.3.3/libnmstate/nm/profile.py
--- nmstate-1.3.3.old/libnmstate/nm/profile.py	2023-02-15 15:48:11.748846029 +0800
+++ nmstate-1.3.3/libnmstate/nm/profile.py	2023-02-15 15:48:45.122101559 +0800
@@ -139,6 +139,11 @@
         else:
             return ""
 
+    def disable_autoconnect(self):
+        if self._nm_simple_conn:
+            nm_conn_setting = self._nm_simple_conn.get_setting_connection()
+            nm_conn_setting.props.autoconnect = False
+
     def update_controller(self, controller):
         nm_simple_conn_update_controller(self._nm_simple_conn, controller)
 
@@ -186,8 +191,10 @@
         elif self._iface.is_down:
             if self._nm_ac:
                 self._add_action(NmProfile.ACTION_DEACTIVATE)
-            elif self._iface.is_virtual and self._nm_dev:
+            if self._iface.is_virtual and self._nm_dev:
                 self._add_action(NmProfile.ACTION_DELETE_DEVICE)
+            if self._nm_dev and not self._nm_dev.get_managed():
+                self._add_action(NmProfile.ACTION_DEACTIVATE)
 
         if self._iface.raw.get(ROUTE_REMOVED):
             # This is a workaround for NM bug:
@@ -276,7 +283,12 @@
         )
 
     def prepare_config(self, save_to_disk, gen_conf_mode=False):
-        if self._iface.is_absent or self._iface.is_down:
+        if self._iface.is_absent or (
+            self._iface.is_down
+            and not gen_conf_mode
+            and self._nm_dev
+            and self._nm_dev.get_managed()
+        ):
             return
 
         # Don't create new profile if original desire does not ask
@@ -312,7 +324,9 @@
         self._gen_actions()
         if not self.has_pending_change:
             return
-        if self._iface.is_absent or self._iface.is_down:
+        if self._iface.is_absent or (
+            self._iface.is_down and self._nm_dev and self._nm_dev.get_managed()
+        ):
             return
         # Don't create new profile if original desire does not ask
         # anything besides state:up and not been marked as changed.
@@ -411,6 +425,9 @@
     def _deactivate(self):
         if self._deactivated:
             return
+        self._nm_ac = (
+            self._nm_dev.get_active_connection() if self._nm_dev else None
+        )
         if self._nm_ac:
             ActiveConnectionDeactivate(
                 self._ctx, self._iface.name, self._iface.type, self._nm_ac
diff -Nur nmstate-1.3.3.old/libnmstate/nm/profiles.py nmstate-1.3.3/libnmstate/nm/profiles.py
--- nmstate-1.3.3.old/libnmstate/nm/profiles.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nm/profiles.py	2023-02-15 15:48:45.122101559 +0800
@@ -26,6 +26,7 @@
 from libnmstate.schema import Interface
 from libnmstate.schema import InterfaceState
 from libnmstate.schema import InterfaceType
+from libnmstate.schema import OvsDB
 
 from .common import NM
 from .device import is_externally_managed
@@ -51,9 +52,11 @@
         _append_nm_ovs_port_iface(net_state)
         all_profiles = []
         for iface in net_state.ifaces.all_ifaces():
-            if iface.is_up:
+            if iface.is_up or iface.is_down:
                 profile = NmProfile(self._ctx, iface)
                 profile.prepare_config(save_to_disk=False, gen_conf_mode=True)
+                if iface.is_down:
+                    profile.disable_autoconnect()
                 all_profiles.append(profile)
 
         _use_uuid_as_controller_and_parent(all_profiles)
@@ -123,16 +126,18 @@
     subordinate of NM OVS port profile which is port of the OVS bridge
     profile.
     We need to create/delete this NM OVS port profile accordingly.
+    We skip this action if ovs interface is not changed.
     """
     nm_ovs_port_ifaces = {}
 
     for iface in net_state.ifaces.all_kernel_ifaces.values():
         if iface.controller_type == InterfaceType.OVS_BRIDGE:
+            has_ovs_change = _has_ovs_changes(iface, net_state)
             nm_ovs_port_iface = create_iface_for_nm_ovs_port(iface)
             iface.set_controller(
                 nm_ovs_port_iface.name, InterfaceType.OVS_PORT
             )
-            if iface.is_desired or iface.is_changed:
+            if (iface.is_desired or iface.is_changed) and has_ovs_change:
                 nm_ovs_port_iface.mark_as_changed()
             nm_ovs_port_ifaces[nm_ovs_port_iface.name] = nm_ovs_port_iface
 
@@ -390,6 +395,10 @@
         iface = nm_profile.iface
         if not iface.is_up:
             continue
+        # InfiniBand setting does not support UUID as parent
+        if iface.type == InterfaceType.INFINIBAND:
+            continue
+
         if (
             iface.controller
             and (iface.is_changed or iface.is_desired)
@@ -434,3 +443,28 @@
             ):
                 return True
     return False
+
+
+def _has_ovs_changes(iface, net_state):
+    """
+    Return False only when below all matches:
+    * Desired interface is up
+    * Desire state did not mentioned its OVS bridge controller
+    * Interface has no changed to controller property
+    * Interface has no ovs-db setting change in desire state
+    """
+    ctrl_iface = net_state.ifaces.get_iface(
+        iface.controller, InterfaceType.OVS_BRIDGE
+    )
+    if (
+        iface.is_desired
+        and iface.is_up
+        and ctrl_iface
+        and not ctrl_iface.is_desired
+        and not ctrl_iface.is_changed
+        and Interface.CONTROLLER not in iface.original_desire_dict
+        and OvsDB.KEY not in iface.original_desire_dict
+    ):
+        return False
+
+    return True
diff -Nur nmstate-1.3.3.old/libnmstate/nmstate.py nmstate-1.3.3/libnmstate/nmstate.py
--- nmstate-1.3.3.old/libnmstate/nmstate.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/nmstate.py	2023-02-15 15:48:33.672042194 +0800
@@ -73,6 +73,7 @@
     include_status_data=None,
     info_type=_INFO_TYPE_RUNNING,
     include_secrets=False,
+    include_controller_prop=True,
 ):
     for plugin in plugins:
         plugin.refresh_content()
@@ -81,7 +82,7 @@
         report["capabilities"] = plugins_capabilities(plugins)
 
     report[Interface.KEY] = _get_interface_info_from_plugins(
-        plugins, info_type
+        plugins, info_type, include_controller_prop=include_controller_prop
     )
 
     report[Route.KEY] = _get_routes_from_plugins(plugins, info_type)
@@ -103,6 +104,7 @@
 
     if not include_secrets:
         hide_the_secrets(report)
+
     return report
 
 
@@ -185,7 +187,9 @@
     return chose_plugin
 
 
-def _get_interface_info_from_plugins(plugins, info_type):
+def _get_interface_info_from_plugins(
+    plugins, info_type, include_controller_prop=True
+):
     all_ifaces = {}
     IFACE_PRIORITY_METADATA = "_plugin_priority"
     IFACE_PLUGIN_SRC_METADATA = "_plugin_source"
@@ -287,6 +291,8 @@
     for iface in all_ifaces.values():
         iface.pop(IFACE_PRIORITY_METADATA)
         iface.pop(IFACE_PLUGIN_SRC_METADATA)
+        if not include_controller_prop:
+            iface.pop(Interface.CONTROLLER, None)
 
     return sorted(all_ifaces.values(), key=itemgetter(Interface.NAME))
 
@@ -404,6 +410,7 @@
         plugins,
         info_type=_INFO_TYPE_RUNNING_CONFIG,
         include_secrets=include_secrets,
+        include_controller_prop=False,
     )
 
 
diff -Nur nmstate-1.3.3.old/libnmstate/plugin.py nmstate-1.3.3/libnmstate/plugin.py
--- nmstate-1.3.3.old/libnmstate/plugin.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/plugin.py	2023-02-14 18:35:05.960135769 +0800
@@ -32,6 +32,7 @@
     PLUGIN_CAPABILITY_ROUTE = "route"
     PLUGIN_CAPABILITY_ROUTE_RULE = "route_rule"
     PLUGIN_CAPABILITY_DNS = "dns"
+    PLUGIN_CAPABILITY_OVSDB_GLOBAL = "ovsdb_global"
 
     DEFAULT_PRIORITY = 10
 
diff -Nur nmstate-1.3.3.old/libnmstate/plugins/nmstate_plugin_ovsdb.py nmstate-1.3.3/libnmstate/plugins/nmstate_plugin_ovsdb.py
--- nmstate-1.3.3.old/libnmstate/plugins/nmstate_plugin_ovsdb.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/plugins/nmstate_plugin_ovsdb.py	2023-02-14 18:35:05.960135769 +0800
@@ -156,8 +156,16 @@
         return NmstatePlugin.DEFAULT_PRIORITY + 1
 
     @property
+    def capabilities(self):
+        return [
+            NmstatePlugin.PLUGIN_CAPABILITY_OVSDB_GLOBAL,
+        ]
+
+    @property
     def plugin_capabilities(self):
-        return NmstatePlugin.PLUGIN_CAPABILITY_IFACE
+        return [
+            NmstatePlugin.PLUGIN_CAPABILITY_IFACE,
+        ]
 
     def get_interfaces(self):
         ifaces = []
diff -Nur nmstate-1.3.3.old/libnmstate/schema.py nmstate-1.3.3/libnmstate/schema.py
--- nmstate-1.3.3.old/libnmstate/schema.py	2023-02-15 15:48:11.742846002 +0800
+++ nmstate-1.3.3/libnmstate/schema.py	2023-02-15 15:48:33.672042194 +0800
@@ -48,6 +48,7 @@
     MTU = "mtu"
     COPY_MAC_FROM = "copy-mac-from"
     ACCEPT_ALL_MAC_ADDRESSES = "accept-all-mac-addresses"
+    CONTROLLER = "controller"
 
 
 class Route:
@@ -148,6 +149,7 @@
     AUTO_GATEWAY = "auto-gateway"
     AUTO_ROUTES = "auto-routes"
     AUTO_ROUTE_TABLE_ID = "auto-route-table-id"
+    AUTO_ROUTE_METRIC = "auto-route-metric"
 
 
 class InterfaceIPv4(InterfaceIP):
diff -Nur nmstate-1.3.3.old/libnmstate/validator.py nmstate-1.3.3/libnmstate/validator.py
--- nmstate-1.3.3.old/libnmstate/validator.py	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/validator.py	2023-02-14 18:35:05.960135769 +0800
@@ -23,6 +23,7 @@
 import jsonschema as js
 
 from libnmstate.schema import Interface
+from libnmstate.schema import OvsDB
 from libnmstate.schema import InterfaceType
 from libnmstate.error import NmstateDependencyError
 
@@ -43,6 +44,7 @@
 
 def validate_capabilities(state, capabilities):
     validate_interface_capabilities(state.get(Interface.KEY, []), capabilities)
+    validate_ovsdb_global_cap(state.get(OvsDB.KEY, {}), capabilities)
 
 
 def validate_interface_capabilities(ifaces_state, capabilities):
@@ -78,3 +80,15 @@
             "Interfaces count exceeds the limit %s in desired state",
             MAX_SUPPORTED_INTERFACES,
         )
+
+
+def validate_ovsdb_global_cap(ovsdb_global_conf, capabilities):
+    if (
+        ovsdb_global_conf
+        and NmstatePlugin.PLUGIN_CAPABILITY_OVSDB_GLOBAL not in capabilities
+    ):
+        raise NmstateDependencyError(
+            "Missing plugin for ovs-db global configuration, "
+            "please try to install 'nmstate-plugin-ovsdb' or other plugin "
+            "provides NmstatePlugin.PLUGIN_CAPABILITY_OVSDB_GLOBAL"
+        )
diff -Nur nmstate-1.3.3.old/libnmstate/VERSION nmstate-1.3.3/libnmstate/VERSION
--- nmstate-1.3.3.old/libnmstate/VERSION	2022-08-11 23:22:22.000000000 +0800
+++ nmstate-1.3.3/libnmstate/VERSION	2023-02-15 15:48:33.668708842 +0800
@@ -1 +1 @@
-1.3.3
+1.3.4