diff --git a/SOURCES/BZ_2169642-Fix-SRIOV-2.patch b/SOURCES/BZ_2169642-Fix-SRIOV-2.patch new file mode 100644 index 0000000..8f33243 --- /dev/null +++ b/SOURCES/BZ_2169642-Fix-SRIOV-2.patch @@ -0,0 +1,206 @@ +From 12e298f27f1ffa58f6f7e60016ff197719b7a26e Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Thu, 23 Feb 2023 13:06:01 +0800 +Subject: [PATCH] nm: Fix error on SR-IOV + +When SR-IOV VF naming scheme is like `ens1f0v0`, nmstate will delete +the VF NM connection when applying this state: + +```yml +--- +interfaces: +- name: ens1f0 + type: ethernet + state: up + ethernet: + sr-iov: + total-vfs: 1 +- name: ens1f0v0 + type: ethernet + state: up + ipv4: + enabled: false + ipv6: + enabled: false +``` + +This is because `delete_other_profiles()` is checking +`self._nm_profile()` from active NM profile instead of newly created +one. The fix is using newly created profile `self._nm_simple_conn`. + +We also have race problem when activating PF along with VF, PF +activation might delete VF NIC which cause VF activation failed. To +workaround that, we activate PF first via `NmProfile.ACTION_SRIOV_PF` +and wait on it before start VF activation. + +Also problem found during SR-IOV investigations is we do extra +un-required modification to `NM.SettingOvsExternalIDs` even it is not +mentioned in desired. We skip overriding `NM.SettingOvsExternalIDs` when +not desired. + +Existing test case can cover the use cases. + +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ifaces.py | 18 +++++++++++++++++- + libnmstate/netapplier.py | 20 +++++++++++--------- + libnmstate/nm/connection.py | 2 +- + libnmstate/nm/profile.py | 12 ++++++++++-- + 4 files changed, 39 insertions(+), 13 deletions(-) + +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index 828ff578..470dc0e6 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -157,6 +157,23 @@ class Ifaces: + def has_vf_count_change_and_missing_eth(self): + return self._has_vf_count_change() and self._has_missing_veth() + ++ def has_sriov_iface(self): ++ for iface in self.all_kernel_ifaces.values(): ++ if (iface.is_desired or iface.is_changed) and iface.is_up: ++ cur_iface = self._cur_kernel_ifaces.get(iface.name) ++ if ( ++ cur_iface ++ and cur_iface.raw.get(Ethernet.CONFIG_SUBTREE, {}).get( ++ Ethernet.SRIOV_SUBTREE, {} ++ ) ++ ) or iface.original_desire_dict.get( ++ Ethernet.CONFIG_SUBTREE, {} ++ ).get( ++ Ethernet.SRIOV_SUBTREE, {} ++ ): ++ return True ++ return False ++ + def _has_vf_count_change(self): + for iface in self.all_kernel_ifaces.values(): + cur_iface = self._cur_kernel_ifaces.get(iface.name) +@@ -664,7 +681,6 @@ class Ifaces: + return None + + def get_cur_iface(self, iface_name, iface_type): +- + iface = self._cur_kernel_ifaces.get(iface_name) + if iface and iface_type in (None, InterfaceType.UNKNOWN, iface.type): + return iface +diff --git a/libnmstate/netapplier.py b/libnmstate/netapplier.py +index ae909126..50a70a9c 100644 +--- a/libnmstate/netapplier.py ++++ b/libnmstate/netapplier.py +@@ -104,7 +104,7 @@ def apply( + pf_net_state, + verify_change, + save_to_disk, +- has_sriov_pf=True, ++ VERIFY_RETRY_COUNT_SRIOV, + ) + # Refresh the current state + current_state = show_with_plugins( +@@ -120,8 +120,16 @@ def apply( + current_state, + save_to_disk, + ) ++ ++ if net_state.ifaces.has_sriov_iface(): ++ # 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 ++ else: ++ verify_retry = VERIFY_RETRY_COUNT ++ + _apply_ifaces_state( +- plugins, net_state, verify_change, save_to_disk, has_sriov_pf=False ++ plugins, net_state, verify_change, save_to_disk, verify_retry + ) + if commit: + destroy_checkpoints(plugins, checkpoints) +@@ -154,7 +162,7 @@ def rollback(*, checkpoint=None): + + + def _apply_ifaces_state( +- plugins, net_state, verify_change, save_to_disk, has_sriov_pf=False ++ plugins, net_state, verify_change, save_to_disk, verify_retry + ): + for plugin in plugins: + # Do not allow plugin to modify the net_state for future verification +@@ -163,12 +171,6 @@ def _apply_ifaces_state( + + verified = False + if verify_change: +- 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 +- else: +- verify_retry = VERIFY_RETRY_COUNT + for _ in range(verify_retry): + try: + _verify_change(plugins, net_state) +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 1fbb380b..6448e372 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -240,7 +240,7 @@ def create_new_nm_simple_conn(iface, nm_profile): + InterfaceType.OVS_PORT, + ) + or iface.type == InterfaceType.OVS_BRIDGE +- ): ++ ) and OvsDB.OVS_DB_SUBTREE in iface.original_desire_dict: + nm_setting = create_ovsdb_external_ids_setting( + iface_info.get(OvsDB.OVS_DB_SUBTREE, {}) + ) +diff --git a/libnmstate/nm/profile.py b/libnmstate/nm/profile.py +index 53eaebed..ad1ad19f 100644 +--- a/libnmstate/nm/profile.py ++++ b/libnmstate/nm/profile.py +@@ -56,6 +56,7 @@ ROUTE_REMOVED = "_route_removed" + class NmProfile: + # For unmanged iface and desired to down + ACTION_ACTIVATE_FIRST = "activate_first" ++ ACTION_SRIOV_PF = "activate_sriov_pf" + ACTION_DEACTIVATE = "deactivate" + ACTION_DEACTIVATE_FIRST = "deactivate_first" + ACTION_DELETE_DEVICE = "delete_device" +@@ -77,6 +78,7 @@ class NmProfile: + ACTION_ACTIVATE_FIRST, + ACTION_DEACTIVATE_FIRST, + ACTION_TOP_CONTROLLER, ++ ACTION_SRIOV_PF, + ACTION_NEW_IFACES, + ACTION_OTHER_CONTROLLER, + ACTION_NEW_OVS_PORT, +@@ -181,6 +183,11 @@ class NmProfile: + else: + self._add_action(NmProfile.ACTION_NEW_IFACES) + else: ++ if ( ++ self._nm_dev.props.capabilities ++ & NM.DeviceCapabilities.SRIOV ++ ): ++ self._add_action(NmProfile.ACTION_SRIOV_PF) + if self._iface.type == InterfaceType.OVS_PORT: + self._add_action(NmProfile.ACTION_MODIFIED_OVS_PORT) + if self._iface.type == InterfaceType.OVS_INTERFACE: +@@ -462,6 +469,7 @@ class NmProfile: + + def do_action(self, action): + if action in ( ++ NmProfile.ACTION_SRIOV_PF, + NmProfile.ACTION_MODIFIED, + NmProfile.ACTION_MODIFIED_OVS_PORT, + NmProfile.ACTION_MODIFIED_OVS_IFACE, +@@ -559,8 +567,8 @@ class NmProfile: + or nm_profile.get_connection_type() == self._nm_iface_type + ) + and ( +- self._nm_profile is None +- or nm_profile.get_uuid() != self._nm_profile.get_uuid() ++ self._nm_simple_conn is None ++ or nm_profile.get_uuid() != self._nm_simple_conn.get_uuid() + ) + ): + ProfileDelete( +-- +2.39.2 + diff --git a/SOURCES/BZ_2169642-Fix-sriov.patch b/SOURCES/BZ_2169642-Fix-sriov.patch new file mode 100644 index 0000000..990f1bf --- /dev/null +++ b/SOURCES/BZ_2169642-Fix-sriov.patch @@ -0,0 +1,1075 @@ +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 . +-# ++# 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 diff --git a/SOURCES/BZ_2169642-Ignore-error-when-creating-profile-if-not-desired.patch b/SOURCES/BZ_2169642-Ignore-error-when-creating-profile-if-not-desired.patch new file mode 100644 index 0000000..68e34d4 --- /dev/null +++ b/SOURCES/BZ_2169642-Ignore-error-when-creating-profile-if-not-desired.patch @@ -0,0 +1,66 @@ +From 030bd5e38a2913e96ef145f88cb74c619acea6bf Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 27 Feb 2023 12:17:05 +0800 +Subject: [PATCH] nm: Ignore error when creating profile if not desired + +When a undesired interface holding `autoconf: true` and `dhcp: false` +for IPv6, nmstate will fail with error: + + Autoconf without DHCP is not supported yet + +This is caused by `nm/connection.py` try to create `NM.SimpleConnection` +for every interface even not desired. + +This patch changed to: + * Only create new `NM.SimpleConnection` when desired or changed. + * Use current profile if exists when not desired or changed. + * Ignore error if not desired/changed. + +Integration test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/profile.py | 20 +++++++++++++++++--- + 1 file changed, 17 insertions(+), 3 deletions(-) + +diff --git a/libnmstate/nm/profile.py b/libnmstate/nm/profile.py +index ad1ad19f..1119cd1a 100644 +--- a/libnmstate/nm/profile.py ++++ b/libnmstate/nm/profile.py +@@ -24,6 +24,7 @@ from distutils.version import StrictVersion + import logging + import time + ++from libnmstate.error import NmstateError + from libnmstate.error import NmstateInternalError + from libnmstate.error import NmstateLibnmError + from libnmstate.error import NmstateNotSupportedError +@@ -321,9 +322,22 @@ class NmProfile: + # TODO: Use applied config as base profile + # Or even better remove the base profile argument as top level + # of nmstate should provide full/merged configure. +- self._nm_simple_conn = create_new_nm_simple_conn( +- self._iface, self._nm_profile +- ) ++ if self._iface.is_changed or self._iface.is_desired: ++ self._nm_simple_conn = create_new_nm_simple_conn( ++ self._iface, self._nm_profile ++ ) ++ elif self._nm_profile: ++ self._nm_simple_conn = NM.SimpleConnection.new_clone( ++ self._nm_profile ++ ) ++ else: ++ try: ++ self._nm_simple_conn = create_new_nm_simple_conn( ++ self._iface, self._nm_profile ++ ) ++ # No error for undesired interface ++ except NmstateError: ++ pass + + def save_config(self, save_to_disk): + self._check_sriov_support() +-- +2.39.2 + diff --git a/SOURCES/BZ_2170078-Introduce-wait-ip.patch b/SOURCES/BZ_2170078-Introduce-wait-ip.patch new file mode 100644 index 0000000..b8ebc60 --- /dev/null +++ b/SOURCES/BZ_2170078-Introduce-wait-ip.patch @@ -0,0 +1,208 @@ +From ef23275126898f316cb3e7e2df552c006e867105 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Tue, 14 Feb 2023 15:20:21 +0800 +Subject: [PATCH] ip: Introduce `Interface.WAIT_IP` + +This patch is introducing `Interface.WAIT_IP` property with these +possible values: + * `Interface.WAIT_IP_ANY`("any"): + System will consider interface activated when any IP stack is + configured(neither static or automatic). + * `Interface.WAIT_IP_IPV4`("ipv4"): + System will wait IPv4 been configured. + * `Interface.WAIT_IP_IPV6`("ipv6"): + System will wait IPv6 been configured. + * `Introduce.WAIT_IP_IPV4_AND_IPV6`("ipv4+ipv6"): + System will wait both IPv4 and IPv6 been configured. + +Considering this old branch, there is no validation on invalid use cases +like setting wait-ip on disabled IP stack. + +Example YAML on waiting both IP stacks: + +```yml +--- +interfaces: + - name: eth1 + type: ethernet + state: up + mtu: 1500 + wait-ip: ipv4+ipv6 + ipv4: + enabled: true + dhcp: true + ipv6: + enabled: true + dhcp: true + autoconf: true +``` + +Integration test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/connection.py | 9 +++---- + libnmstate/nm/ip.py | 48 +++++++++++++++++++++++++++++++++++++ + libnmstate/nm/plugin.py | 29 +++++++++------------- + libnmstate/schema.py | 5 ++++ + 4 files changed, 69 insertions(+), 22 deletions(-) + create mode 100644 libnmstate/nm/ip.py + +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 1974cfd4..1fbb380b 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -39,6 +39,7 @@ from .common import NM + from .ethtool import create_ethtool_setting + from .ieee_802_1x import create_802_1x_setting + from .infiniband import create_setting as create_infiniband_setting ++from .ip import set_wait_ip + from .ipv4 import create_setting as create_ipv4_setting + from .ipv6 import create_setting as create_ipv6_setting + from .lldp import apply_lldp_setting +@@ -106,10 +107,10 @@ class _ConnectionSetting: + def create_new_nm_simple_conn(iface, nm_profile): + nm_iface_type = Api2Nm.get_iface_type(iface.type) + iface_info = iface.to_dict() +- settings = [ +- create_ipv4_setting(iface_info.get(Interface.IPV4), nm_profile), +- create_ipv6_setting(iface_info.get(Interface.IPV6), nm_profile), +- ] ++ ipv4_set = create_ipv4_setting(iface_info.get(Interface.IPV4), nm_profile) ++ ipv6_set = create_ipv6_setting(iface_info.get(Interface.IPV6), nm_profile) ++ set_wait_ip(ipv4_set, ipv6_set, iface_info.get(Interface.WAIT_IP)) ++ settings = [ipv4_set, ipv6_set] + con_setting = _ConnectionSetting() + if nm_profile and not is_multiconnect_profile(nm_profile): + con_setting.import_by_profile(nm_profile, iface.is_controller) +diff --git a/libnmstate/nm/ip.py b/libnmstate/nm/ip.py +new file mode 100644 +index 00000000..d0fc1e3b +--- /dev/null ++++ b/libnmstate/nm/ip.py +@@ -0,0 +1,48 @@ ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ ++from libnmstate.schema import Interface ++ ++ ++def get_wait_ip(applied_config): ++ if applied_config: ++ nm_ipv4_may_fail = _get_may_fail(applied_config, False) ++ nm_ipv6_may_fail = _get_may_fail(applied_config, True) ++ if nm_ipv4_may_fail and not nm_ipv6_may_fail: ++ return Interface.WAIT_IP_IPV6 ++ elif not nm_ipv4_may_fail and nm_ipv6_may_fail: ++ return Interface.WAIT_IP_IPV4 ++ elif not nm_ipv4_may_fail and not nm_ipv6_may_fail: ++ return Interface.WAIT_IP_IPV4_AND_IPV6 ++ return Interface.WAIT_IP_ANY ++ ++ ++def set_wait_ip(nm_ipv4_set, nm_ipv6_set, wait_ip): ++ if nm_ipv4_set: ++ if wait_ip == Interface.WAIT_IP_ANY: ++ nm_ipv4_set.props.may_fail = True ++ elif wait_ip in ( ++ Interface.WAIT_IP_IPV4, ++ Interface.WAIT_IP_IPV4_AND_IPV6, ++ ): ++ nm_ipv4_set.props.may_fail = False ++ if nm_ipv6_set: ++ if wait_ip == Interface.WAIT_IP_ANY: ++ nm_ipv6_set.props.may_fail = True ++ elif wait_ip in ( ++ Interface.WAIT_IP_IPV6, ++ Interface.WAIT_IP_IPV4_AND_IPV6, ++ ): ++ nm_ipv6_set.props.may_fail = False ++ ++ ++def _get_may_fail(nm_profile, is_ipv6): ++ if is_ipv6: ++ nm_set = nm_profile.get_setting_ip6_config() ++ else: ++ nm_set = nm_profile.get_setting_ip4_config() ++ ++ if nm_set: ++ return nm_set.props.may_fail ++ else: ++ # NM is defaulting `may-fail` as True ++ return True +diff --git a/libnmstate/nm/plugin.py b/libnmstate/nm/plugin.py +index bca1aedd..9bbbbb98 100644 +--- a/libnmstate/nm/plugin.py ++++ b/libnmstate/nm/plugin.py +@@ -1,21 +1,5 @@ +-# +-# 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 . +-# ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ + from distutils.version import StrictVersion + import logging + from operator import itemgetter +@@ -25,6 +9,8 @@ from libnmstate.error import NmstateNotSupportedError + from libnmstate.error import NmstateValueError + from libnmstate.schema import DNS + from libnmstate.schema import Interface ++from libnmstate.schema import InterfaceIPv4 ++from libnmstate.schema import InterfaceIPv6 + from libnmstate.schema import InterfaceType + from libnmstate.schema import LLDP + from libnmstate.plugin import NmstatePlugin +@@ -41,6 +27,7 @@ from .device import list_devices + from .dns import get_running as get_dns_running + from .dns import get_running_config as get_dns_running_config + from .infiniband import get_info as get_infiniband_info ++from .ip import get_wait_ip + from .ipv4 import get_info as get_ipv4_info + from .ipv6 import get_info as get_ipv6_info + from .lldp import get_info as get_lldp_info +@@ -190,6 +177,12 @@ class NetworkManagerPlugin(NmstatePlugin): + + if applied_config: + iface_info.update(get_ovsdb_external_ids(applied_config)) ++ if iface_info.get(Interface.IPV4, {}).get( ++ InterfaceIPv4.ENABLED ++ ) or iface_info.get(Interface.IPV6, {}).get( ++ InterfaceIPv6.ENABLED ++ ): ++ iface_info[Interface.WAIT_IP] = get_wait_ip(applied_config) + + info.append(iface_info) + +diff --git a/libnmstate/schema.py b/libnmstate/schema.py +index e740abff..c3d3fcfc 100644 +--- a/libnmstate/schema.py ++++ b/libnmstate/schema.py +@@ -49,6 +49,11 @@ class Interface: + COPY_MAC_FROM = "copy-mac-from" + ACCEPT_ALL_MAC_ADDRESSES = "accept-all-mac-addresses" + CONTROLLER = "controller" ++ WAIT_IP = "wait-ip" ++ WAIT_IP_ANY = "any" ++ WAIT_IP_IPV4 = "ipv4" ++ WAIT_IP_IPV6 = "ipv6" ++ WAIT_IP_IPV4_AND_IPV6 = "ipv4+ipv6" + + + class Route: +-- +2.39.2 + diff --git a/SPECS/nmstate.spec b/SPECS/nmstate.spec index a29ece7..a658ac7 100644 --- a/SPECS/nmstate.spec +++ b/SPECS/nmstate.spec @@ -4,7 +4,7 @@ Name: nmstate Version: 1.3.3 -Release: 4%{?dist} +Release: 8%{?dist} Summary: Declarative network manager API License: LGPLv2+ URL: https://github.com/%{srcname}/%{srcname} @@ -18,6 +18,10 @@ Patch10: BZ_2139698-nm-sriov-Do-not-touch-SR-IOV-if-not-desired.patch Patch11: BZ_2128555-ip-allow-extra-IP-address-found-in-verification-stag.patch Patch12: BZ_2149048-ip-Preserve-the-IP-address-order-when-applying.patch Patch13: BZ_2150705-nm-fix-activation-retry.patch +Patch14: BZ_2169642-Fix-sriov.patch +Patch15: BZ_2170078-Introduce-wait-ip.patch +Patch16: BZ_2169642-Fix-SRIOV-2.patch +Patch17: BZ_2169642-Ignore-error-when-creating-profile-if-not-desired.patch BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: gnupg2 @@ -152,6 +156,18 @@ popd /sbin/ldconfig %changelog +* Mon Feb 27 2023 Gris Ge - 1.3.3-8 +- Ignore error in undesired iface. RHBZ#2169642 + +* Thu Feb 23 2023 Gris Ge - 1.3.3-7 +- New patch for fixing SR-IOV. RHBZ#2169642 + +* Thu Feb 16 2023 Gris Ge - 1.3.3-6 +- Add Interface wait-ip support. RHBZ#2170078 + +* Wed Feb 15 2023 Gris Ge - 1.3.3-5 +- Fix SR-IOV creating VF with IP stack disabled. RHBZ#2169642 + * Tue Dec 06 2022 Gris Ge - 1.3.3-4 - Fix activation retry. RHBZ#2150705