Repository: RedHatQE/openshift-virtualization-tests
Pull request #218: Utilities - Add NodeNetworkConfigurationPolicy resource to libs/net
Author: login: Anatw
Labels: name: verified
name: size/L
name: can-be-merged
name: branch-main
name: approved-dbasunag
name: tox:verify-tc-requirement-polarion:passed
description: D/S Testing
name: lgtm-polarion-jenkins
name: lgtm-yossisegev
name: commented-Anatw
name: commented-EdDev
name: tox:verify-bugs-are-open:passed
description: D/S Testing
name: lgtm-EdDev
name: sig-network
name: approved-vsibirsk
name: lgtm-rnetser
Created: 2025-02-03T09:03:24Z | Merged: 2025-04-10T13:41:19Z

##### Short description:
Introducing a new NodeNetworkConfigurationPolicy (NNCP) resource to the libs/net library. This resource is meant to replace usage oc NNCP in the wrapper, which has large amount of functions added to it, which makes it difficult to add new helpers and functionalities.

##### More details:
The new NNCP is used in an existing test - test_linux_bridge_functionality.

##### What this PR does / why we need it:
As part of the initiative to create simpler resources and reduce dependency, this PR introduces a new NNCP resource in the lib directory.




Diff:
diff --git a/tests/network/cluster_addons_operator/test_network_addons_operator.py b/tests/network/cluster_addons_operator/test_network_addons_operator.py
index 58f7fc242e..466b45bd02 100644
--- a/tests/network/cluster_addons_operator/test_network_addons_operator.py
+++ b/tests/network/cluster_addons_operator/test_network_addons_operator.py
@@ -17,6 +17,16 @@
 
 import utilities.network
 from tests.network.constants import EXPECTED_CNAO_COMP_NAMES
+from tests.network.libs.nodenetworkconfigurationpolicy import (
+    STP,
+    Bridge,
+    BridgeOptions,
+    DesiredState,
+    Interface,
+    IPv4,
+    IPv6,
+    NodeNetworkConfigurationPolicy,
+)
 from utilities.constants import CLUSTER_NETWORK_ADDONS_OPERATOR, LINUX_BRIDGE
 from utilities.infra import get_node_selector_dict
 from utilities.virt import VirtualMachineForTests, fedora_vm_body
@@ -149,21 +159,41 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = next(
+        (
+            interface.name
+            for interface in net_add_op_bridge_device.desired_state_spec.interfaces
+            if interface.type == LINUX_BRIDGE
+        ),
+    )
+
     with utilities.network.network_nad(
         nad_type=LINUX_BRIDGE,
-        nad_name=net_add_op_bridge_device.bridge_name,
-        interface_name=net_add_op_bridge_device.bridge_name,
+        nad_name=bridge_name,
+        interface_name=bridge_name,
         namespace=namespace,
     ) as nad:
         yield nad
diff --git a/tests/network/libs/nodenetworkconfigurationpolicy.py b/tests/network/libs/nodenetworkconfigurationpolicy.py
new file mode 100644
index 0000000000..b5db8376c8
--- /dev/null
+++ b/tests/network/libs/nodenetworkconfigurationpolicy.py
@@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
+            iface["state"] = Nncp.Interface.State.ABSENT
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()


Reviews:
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6320
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T09:07:58Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6324
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T09:16:21Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6326
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T09:23:40Z
user: login: yossisegev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T11:36:02Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T12:42:51Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T12:55:21Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T13:58:14Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6338
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T14:00:44Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6340
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T14:04:33Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6342
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-03T14:07:13Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T14:19:13Z
user: login: yossisegev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T14:20:59Z
user: login: EdDev
body: I have not covered everything.
Try to treat the points raised until now and I'll revisit to continue then.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-05T18:12:01Z
user: login: dbasunag
body: Why would we want to duplicate wrapper code to the test repo? I am not sure I understand what you are trying to do.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T19:45:14Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-12T14:00:01Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6815
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-12T14:02:58Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T07:16:15Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T07:41:45Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T07:59:25Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T08:07:15Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T08:16:49Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T09:32:34Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T13:00:12Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6878
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-13T13:02:59Z
user: login: EdDev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T13:34:42Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:12:28Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:20:44Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:27:01Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:32:28Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:32:50Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:35:14Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:41:57Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:47:18Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T22:02:37Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:14:16Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:17:22Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:18:07Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:36:29Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:46:08Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T06:55:47Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T07:11:40Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T07:14:27Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T13:13:09Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T13:27:45Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T13:31:08Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T13:34:45Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-16T15:58:42Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/6961
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-17T10:26:05Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-17T10:39:10Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T06:24:15Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T06:34:45Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:01:32Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:04:57Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T07:13:56Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T08:37:12Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T09:52:42Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T11:45:46Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-19T11:47:45Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T11:45:01Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7146
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-24T11:56:46Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T06:18:26Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T06:22:17Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7263
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-25T12:18:44Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T15:10:54Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7278
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-25T15:13:10Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:41:19Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:43:30Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T08:50:20Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T13:38:34Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T13:48:01Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T14:08:13Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T14:24:57Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T14:28:34Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T15:05:58Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T17:39:22Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7384
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-02-27T12:05:10Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7388
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:29:54Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T16:35:59Z
user: login: EdDev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T07:47:50Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7516
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T08:43:31Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7534
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-03T15:14:32Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:22:35Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7538
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T15:25:35Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:43:20Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:30:31Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T05:19:38Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T05:53:33Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:05:27Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:21:41Z
user: login: qinqon
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T06:34:27Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:52:59Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:55:02Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:57:57Z
user: login: qinqon
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T06:58:24Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:49:34Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7600
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:24:02Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:14:09Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7623
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:17:15Z
user: login: polarion-jenkins
body: D/S test `tox -e verify-bugs-are-open` passed: cnv-tests-tox-executor/7651
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:51:39Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T19:25:58Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T20:40:58Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:24:57Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:34:55Z
user: login: sbahar619
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T06:05:15Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:30:58Z
user: login: polarion-jenkins
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T11:33:12Z
user: login: EdDev
body: Thank you!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:10:55Z
user: login: yossisegev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:38:31Z
user: login: sbahar619
body: Thanks!!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:59:14Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:22:34Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:27:55Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T15:10:50Z
user: login: EdDev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:14:49Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:43:51Z
user: login: yossisegev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:47:21Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:00:25Z
user: login: myakove
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-06T17:00:37Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:52:48Z
user: login: jpeimer
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:42:55Z
user: login: dbasunag
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T13:50:24Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T14:23:05Z
user: login: polarion-jenkins
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:00:02Z
user: login: polarion-jenkins
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:00:22Z
user: login: polarion-jenkins
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-08T17:03:53Z
user: login: yossisegev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T10:03:37Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:21:21Z
user: login: dbasunag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:44:14Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:52:52Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T11:54:28Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T12:07:53Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T12:12:59Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T12:42:09Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T12:46:12Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T12:47:34Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:15:33Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:19:20Z
user: login: yossisegev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:33:29Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:52:08Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:27:30Z
user: login: polarion-jenkins
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T09:30:18Z
user: login: yossisegev
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:24:41Z
user: login: Anatw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:31:13Z
user: login: EdDev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:38:26Z
user: login: yossisegev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:45:49Z
user: login: rnetser
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T11:43:10Z
user: login: EdDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:01:25Z
user: login: rnetser
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T13:09:19Z
user: login: vsibirsk
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T13:32:13Z
user: login: dbasunag
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:38:54Z

Review comments:
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: What about "Progressing"?
created_at: 2025-02-03T10:15:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: > additional interfaces created

Not only.
The desired-state of an NNCPs can specify various options of node networking, including modifying existing interfaces or removing interfaces.
created_at: 2025-02-03T10:31:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: I'm not familiar with the rules of `typing`, but are you sure the quotation marks are correct.
`deploy()` returns an object of class (type) `NodeNetworkConfigurationPolicy`, but this looks like it returns a string.
created_at: 2025-02-03T10:42:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(
+                    self._desired_state, dict_factory=replace_underscore_with_hyphen_and_filter_none_values
+                ),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors[0]}"
+            if len(errors) > 1:
+                err_msg += errors[-1]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Are you sure that a condition message cannot have more the 2 "sections" (which are split by "->")? If you only look at the last item you might miss stuff.
Maybe it's better to iterate over `errors` and append **all** its components to `err_msg`.
created_at: 2025-02-03T10:59:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(
+                    self._desired_state, dict_factory=replace_underscore_with_hyphen_and_filter_none_values
+                ),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors[0]}"
+            if len(errors) > 1:
+                err_msg += errors[-1]
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err[0]}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
+        try:
+            self._absent_interface()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
+
+
+def replace_underscore_with_hyphen_and_filter_none_values(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: 2 things:
1. The name of this function is confusing. It implies that it only performs some cosmetic actions, but it actually produces a dict out of a list object.
2. The part of replacing underscores seems like the function is covering-up for the callers misbehavior. The caller should create a proper NNCP object, which also includes creating dict keys according to the kubernetes rules.
created_at: 2025-02-03T11:34:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I didn't use it currently, where do you think it is needed?
created_at: 2025-02-03T12:42:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. I'll add to the explanation.
created_at: 2025-02-03T12:46:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: I didn't search where it is needed, I just missed it as it is one of the NNCP condition types.
But if it is not used then you can ignore this comment.
created_at: 2025-02-03T12:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It was added due to an error I got but now that I remove it - there are no complains. I ran it without the quotation marks and it runs fine. Thanks.
created_at: 2025-02-03T13:30:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(
+                    self._desired_state, dict_factory=replace_underscore_with_hyphen_and_filter_none_values
+                ),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors[0]}"
+            if len(errors) > 1:
+                err_msg += errors[-1]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. I'll log all the errors (As well as libnmstate_err 2 lines bellow).
created_at: 2025-02-03T13:48:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(
+                    self._desired_state, dict_factory=replace_underscore_with_hyphen_and_filter_none_values
+                ),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors[0]}"
+            if len(errors) > 1:
+                err_msg += errors[-1]
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err[0]}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
+        try:
+            self._absent_interface()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
+
+
+def replace_underscore_with_hyphen_and_filter_none_values(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: 1. The actual conversion is known from the caller (in to_dict), but the function doesn't care who calls it and should declare itself correctly. Ack, I'll rename.
2. Not exactly, the caller uses dataclass, which behave in a certain non-kuberneted way -> the field names (keys) can only include underscores and not hyphens. Later we need to alter it according to our kubernetes needs.
created_at: 2025-02-03T13:57:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(
+                    self._desired_state, dict_factory=replace_underscore_with_hyphen_and_filter_none_values
+                ),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors[0]}"
+            if len(errors) > 1:
+                err_msg += errors[-1]
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err[0]}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
+        try:
+            self._absent_interface()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
+
+
+def replace_underscore_with_hyphen_and_filter_none_values(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: 2. Ack, thank you for explaining.
created_at: 2025-02-03T14:19:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: For which feature you use this?
created_at: 2025-02-05T12:40:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please use the `retry` decorator instead.
created_at: 2025-02-05T12:41:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: ?!
created_at: 2025-02-05T16:23:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please try to define the caller before the callee.
I prefer to read from top to bottom.
created_at: 2025-02-05T16:27:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: What is the reason you defined `field(default=True)` here and at all other places?
`auto_dns: bool = True` does not work?
created_at: 2025-02-05T16:30:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: ```python
class IP:
    auto_dns: bool = True
    autoconf: bool
    dhcp: bool
    enabled: bool

class IPv4(IP):
  pass

class IPv6(IP):
  pass
```
created_at: 2025-02-05T16:37:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Why do you need a default value here? If the caller wants an empty list, I prefer it is explicitly passed.
created_at: 2025-02-05T16:39:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please explain why the value is `Any` and not a string.
created_at: 2025-02-05T16:59:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: It seems you copied all the implementation from the wrapper project.
You will need to explain why you took each and every part of these here.

E.g. why the deployment needs to wait for anything, it is not consistent with other objects behavior and is very limiting (e.g. we will not be able to deploy and do other things in parallel, we are forced to make it serial).


In general, I expect to see only the bare minimum without a lot of logic.
created_at: 2025-02-05T17:12:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I do not understand why this is needed nor why it needs to be placed in the nncp. It is obviously nnce related.

It is not even clear to me what this does really. Can you explain and reason for it?
created_at: 2025-02-05T17:41:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: 1. "absent" is not a verb. Perhaps "remove" or "delete": `_delete_interfaces`.
2. You should use the new data structure, not the raw dict. When you patch, then convert it to a dict.
3. Use dict comprehension to build the new data.
created_at: 2025-02-05T18:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: The implementation below does not look correct to me.
The non-used arguments do not help as well.
created_at: 2025-02-05T18:05:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
+        try:
+            self._absent_interface()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
+
+
+def list_to_dict_serialize(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: "list to dict"? Say what?
Maybe call it `keyval_normalization` and document what it does (filter out none values and converts key characters from `_` to `-`).
created_at: 2025-02-05T18:09:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +17,16 @@
 from ocp_resources.validating_webhook_config import ValidatingWebhookConfiguration
 
 import utilities.network
+from libs.net.nodenetworkconfigurationpolicy import (
+    STP,
+    Bridge,
+    BridgeOptions,
+    DesiredState,
+    Interfaces,
+    NodeNetworkConfigurationPolicy,
+    _default_desired_ipv4,
+    _default_desired_ipv6,
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: ?!
created_at: 2025-02-05T18:10:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I need it here:
`def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:`
In order to set the return type to NNCP.
created_at: 2025-02-11T10:45:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I can guess you mean the timeout values, but I'm not sure. Can you please write more?
created_at: 2025-02-11T10:46:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-11T10:48:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Works, I'll change it.
created_at: 2025-02-11T12:22:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-11T13:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-11T14:06:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ignored, thanks.
created_at: 2025-02-11T14:07:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: That's a mistake, I'll change it to string.
created_at: 2025-02-11T14:10:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-11T14:17:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +17,16 @@
 from ocp_resources.validating_webhook_config import ValidatingWebhookConfiguration
 
 import utilities.network
+from libs.net.nodenetworkconfigurationpolicy import (
+    STP,
+    Bridge,
+    BridgeOptions,
+    DesiredState,
+    Interfaces,
+    NodeNetworkConfigurationPolicy,
+    _default_desired_ipv4,
+    _default_desired_ipv6,
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: Ack.
created_at: 2025-02-11T15:03:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
+        try:
+            self._absent_interface()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
+
+
+def list_to_dict_serialize(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I tried different names and ended with this one, but I'm not attached to it. How about dict_normalization?
I added a short documentation in the function.
created_at: 2025-02-11T15:07:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I removed all the nnce reasoning part. In the next patch we are left with a plain NNCP that will print success or failure status. Must-gather should handle the logs collection in case of failure.
created_at: 2025-02-12T11:31:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: 1. Ack.
2. Ack, I hope I understood your request correctly.
3. I used list comprehension.
created_at: 2025-02-12T11:35:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It used to shows the full reason for failure, e.g.:
```
        error reconciling NodeNetworkConfigurationPolicy on node net-awax-418-mn-jkqnm-worker-0-gw5gn at desired state apply: "",
         failed to execute nmstatectl apply --no-commit --timeout 480: 'exit status 1' '' '[2025-02-12T08:00:19Z INFO  nmstatectl] Nmstate version: 2.2.38
        Provide file is not valid NetworkState or NetworkPolicy: interfaces: unknown field `allow-extra-patch-ports`, expected one of `options`, `port`, `ports` at line 2 column 1
```
I simplified this function and it now only focuses on the status of the failure.
created_at: 2025-02-12T11:40:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=list_to_dict_serialize),
+                "nodeSelector": self._node_selector,
+            })
+
+    @property
+    def nnces(self) -> list[NodeNetworkConfigurationEnactment]:
+        nnces = []
+        for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
+            if nnce.name.endswith(f".{self.name}"):
+                nnces.append(nnce)
+        return nnces
+
+    @staticmethod
+    def _get_nnce_error_msg(nnce_name: str, nnce_condition: dict) -> str:
+        err_msg = ""
+        nnce_prefix = f"NNCE {nnce_name}"
+        nnce_msg = nnce_condition.get("message")
+        if not nnce_msg:
+            return err_msg
+
+        errors = nnce_msg.split("->")
+        if errors:
+            err_msg += f"{nnce_prefix}: {errors}"
+
+        libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
+        if libnmstate_err:
+            err_msg += f"{nnce_prefix}: {libnmstate_err}"
+
+        return err_msg
+
+    def _get_failed_nnce(self) -> NodeNetworkConfigurationEnactment:
+        for nnce in self.nnces:
+            try:
+                nnce.wait_for_conditions()
+            except TimeoutExpiredError:
+                self.logger.error(f"Failed to get NNCE {nnce.name} status")
+                continue
+
+            for nnce_cond in nnce.instance.status.conditions:
+                if nnce_cond.type == "Failing" and nnce_cond.status == Resource.Condition.Status.TRUE:
+                    yield nnce
+
+    def _process_failed_status(self, failed_condition_reason: str) -> None:
+        last_err_msg = None
+        for failed_nnce in self._get_failed_nnce():
+            nnce_name = failed_nnce.instance.metadata.name
+            nnce_dict = failed_nnce.instance.to_dict()
+            for cond in nnce_dict["status"]["conditions"]:
+                err_msg = self._get_nnce_error_msg(nnce_name=nnce_name, nnce_condition=cond)
+                if err_msg:
+                    last_err_msg = err_msg
+
+        raise NNCPConfigurationFailed(f"Reason: {failed_condition_reason}\n{last_err_msg}")
+
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+
+        samples = TimeoutSampler(
+            wait_timeout=TIMEOUT_90SEC,
+            sleep=TIMEOUT_5SEC,
+            func=lambda: self.instance.status.conditions,
+        )
+        for sample in samples:
+            if sample:
+                for nncp_condition in sample:
+                    if (
+                        nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                        and nncp_condition["status"] == self.Condition.Status.TRUE
+                    ):
+                        self.logger.info(f"NNCP {self.name} configured Successfully")
+                        return sample
+
+                    elif nncp_condition["reason"] == no_match_node_condition_reason:
+                        raise NNCPConfigurationFailed(f"{self.name}. Reason: {no_match_node_condition_reason}")
+
+                    elif nncp_condition["reason"] == failed_condition_reason:
+                        self._process_failed_status(failed_condition_reason=failed_condition_reason)
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _absent_interface(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self.res["spec"]["desiredState"]["interfaces"]:
+                _iface["state"] = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = self.res["spec"]["desiredState"]["interfaces"]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: The non-used parameters are to be aligned with the resource base class. I'll remove them if you find them confusing.
Can you please explain why you find it incorrect?
The logic behind it is :
1. Delete the interfaces (e.g. - set them to absent).
2. Wait for the NNCP to be configured successfully with the new configuration.
3. Delete the nncp.

We can't delete the object without setting the interfaces to absent because it will leave the nodes in the new configuration state instead of reverting it to the original state before the NNCP configuration.
created_at: 2025-02-12T11:50:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: @Anatw You should also resolve this comment (the standard reviewer can't resolve, but the PR owner can).
created_at: 2025-02-12T16:09:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: +1
created_at: 2025-02-13T07:16:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: It is not a must, you can just use it as a string: `-> "NodeNetworkConfigurationPolicy":`
created_at: 2025-02-13T07:34:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Yes, it is about the use of constants that I consistently ask to stop using.
I am surprise when I see them and when asked to invest more into their review.
created_at: 2025-02-13T07:41:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Since _iface is a local variable within a private method, it doesn't need to be prefixed with an underscore.
created_at: 2025-02-13T07:41:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ```suggestion
                self.logger.info(f"{self.kind}/{self.name} configured successfully")
```
created_at: 2025-02-13T07:59:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: We need to check that the status is True in addition to the reason to accurately assess the condition's state.
created_at: 2025-02-13T08:07:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: AFAIK if DHCP is not specified, it is by default enabled.
Ref: https://nmstate.io/devel/yaml_api.html#dhcp

So are you doing something special here or do I misunderstand something?
created_at: 2025-02-13T08:16:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ditto
Plus, does the '\n' really required?
You catch the exception at:
https://github.com/RedHatQE/openshift-virtualization-tests/blob/779cf1f18a8c03e59edc922baed19b97b3871664/libs/net/nodenetworkconfigurationpolicy.py#L168
Then you use the logger library, which handle it by default.
created_at: 2025-02-13T08:16:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Because only a cluster-admin can be used, I think there is no point in passing the client in. It should use whatever is available by default (and this is indeed all known usages I guess).

But if we do have cases where we pass a special client, I will like to understand why.
created_at: 2025-02-13T08:21:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Why you are not using  the `Resource`  `deploy`? Why any special handling is needed here?
created_at: 2025-02-13T08:24:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self._desired_state.interfaces:
+                _iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Add a check to ensure "interfaces" exists in self.res["spec"]["desiredState"] before accessing it to prevent potential KeyError.
For example, in localnet NNCP, we do not set interfaces.
desiredState is required anyway by the API, so no need to check it.
created_at: 2025-02-13T09:32:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-13T12:17:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. This is true for all reasons, I'll add it in the beginning.
created_at: 2025-02-13T12:40:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack for the first part. Regarding the /n, I find it easier to track in the log.
created_at: 2025-02-13T12:44:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-13T12:45:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if (
+                nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                and nncp_condition["status"] == self.Condition.Status.TRUE
+            ):
+                self.logger.info(f"NNCP {self.name} configured Successfully")
+                return nncp_condition
+
+            elif (
+                nncp_condition["reason"] == no_match_node_condition_reason
+                or nncp_condition["reason"] == failed_condition_reason
+            ):
+                raise NNCPConfigurationFailed(
+                    f"NNCP {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for _iface in self._desired_state.interfaces:
+                _iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        return self.res["spec"]["desiredState"]["interfaces"]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I set it to return an empty list if there are not interfaces.
created_at: 2025-02-13T12:49:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I think the name should be singular.
created_at: 2025-02-13T13:21:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Because the prev body returns, this one can be a simpler `if`.

This could be converted to a `matcher`, but not a must.
created_at: 2025-02-13T13:27:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: What is the purpose of these (3) variables?
created_at: 2025-02-13T13:28:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: You are reading this from the raw cash. Is this what you intended or you are expecting to see what is on the NNCP object now? Can you please clarify?
created_at: 2025-02-13T13:31:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Why it is not deleted through the context manager? Why would we want to remove the object through special methods?
created_at: 2025-02-13T13:33:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Is something using this one? I could not detect its usage.
created_at: 2025-02-13T13:34:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: +1 for using 
```
-> "NodeNetworkConfigurationPolicy":
```
Save us from irelevant code imports if it can be achieved just by using quotes, the goal is just to satisfy the CI linter.
created_at: 2025-02-13T14:12:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Just to be clear, the suggestion of @EdDev as I understand is:
```suggestion
class Interface:
```
created_at: 2025-02-13T14:20:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Currently the client is not used anyway, so we can remove it and add it if some caller in future will request it.
created_at: 2025-02-13T14:27:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Since this code is inside a method of the NodeNetworkConfigurationPolicy class, the nncp_ prefix in nncp_conditions seems redundant. The context already makes it clear that these conditions belong to an NNCP. Consider renaming it to just conditions for better readability.
created_at: 2025-02-13T14:32:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ditto
created_at: 2025-02-13T14:32:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Why not using:
self.Condition.Status.TRUE
As you already did previously?
It is more consistent with other related code.
created_at: 2025-02-13T14:35:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I suggest filtering the conditions first based on this logic to eliminate the need for a nested if block:
```
conditions = [
        condition for condition in self.instance.status.conditions if condition["status"] == self.Condition.Status.TRUE
    ]
```
created_at: 2025-02-13T14:41:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I suggest adding a log warning with the exception message so it's clear why an empty list is returned.
created_at: 2025-02-13T14:47:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: This condition check is redundant, you already check for it in the if statement above.
created_at: 2025-02-13T22:01:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Please log the entire condition dictionary before raising the exception. This will expose additional information, such as condition.message, making debugging easier.
created_at: 2025-02-16T06:14:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Not used, consider removing it.
created_at: 2025-02-16T06:17:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: This entire class is not used.
created_at: 2025-02-16T06:18:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The interfaces method is only used in net_add_op_br1test_nad to get the interface name. Instead, I suggest to define a constant to improve clarity, avoid unnecessary calls, and prevent index errors. Since NNCP can have multiple interfaces, setting it explicitly is safer.
created_at: 2025-02-16T06:36:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
+
+
+def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The dict_normalization function is only used in to_dict() and is defined outside the class scope. Consider replacing it with a lambda function inside asdict() to simplify the code and keep the logic contained within the method.
created_at: 2025-02-16T06:46:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The check if self._desired_state.interfaces: is redundant since interfaces is always a list (even if empty). The method can be simplified by removing this condition and directly iterating over self._desired_state.interfaces.
created_at: 2025-02-16T06:55:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I suggest to refactored the _delete_interfaces() method to avoid two loops by combining the state update and dictionary conversion into a single loop. This improves efficiency and keeps the code cleaner.
created_at: 2025-02-16T07:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Currently, the patch will always be executed, regardless of whether self._desired_state.interfaces is empty or not, since the patch logic is outside any conditional control. This could result in unnecessary updates when no interfaces are present.
created_at: 2025-02-16T07:14:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I do not understand this comment.
Try to give an example of how you want it to be used... show the current usage and the suggested one.
created_at: 2025-02-16T13:07:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I think it is clear and a log is surely not a must for a an acceptable scenario.

If it is not clear, the scenario is that the definition has not defined any chances of interfaces, but something else. It is valid not to define the interfaces AFAIK.
created_at: 2025-02-16T13:10:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
+
+
+def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I disagree.
The function is useful for readability to understand what it does. It has a formal doctext to explain itself as well. Inlining it, will be cryptic.

If you insist, you can make it a `staticmethod`, something the maintainers like more.
created_at: 2025-02-16T13:12:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
+
+
+def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ack, staticmethod is fine.
created_at: 2025-02-16T13:27:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ack.
created_at: 2025-02-16T13:31:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I suggest to define constant for the interface name, something like:
```
BRIDGE_NAME = "net-add-br"
```
Then use it directly and remove the interfaces method.
We already know what is the interface name, I don't see reason to fetch it dynamically.
created_at: 2025-02-16T13:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, thank you both.
created_at: 2025-02-16T14:19:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Sure, I'll create local variables for them.
created_at: 2025-02-16T14:22:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: But I want it disabled. I don't need a DHCP, I'm setting static IP addresses.
created_at: 2025-02-16T14:32:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-16T14:35:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Ohh my... all this wording is basically: This method is currently redundant, please see comment at usage.
created_at: 2025-02-16T15:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll remove it.
created_at: 2025-02-16T16:00:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: The first two are specific failure reasons we can fail on. The third one is just for ease of usage. Reading through it again I think that we can change the code to just check if we succeed, and if not, once we get to the timout - print the condition that is true (the reason we failed on).
WDYT?
created_at: 2025-02-16T16:04:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-16T16:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I think both comments are about the same piece of code.
created_at: 2025-02-16T16:09:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-16T16:09:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: You'll still need to iterate through the. For me it is less readable this way. Let's wait for more opinions on this one.
created_at: 2025-02-16T16:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Correct, I forgot to change this part accordingly, thanks!
created_at: 2025-02-16T16:13:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. I not converting it into a matcher yet, waiting to hear your thought on removing this second if as I suggested in a previous comment.
created_at: 2025-02-16T16:32:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I can just log it as part of the exception, no? I'll change the exception to log the entire condition. If you don't agree, please let me know.
created_at: 2025-02-16T16:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, removed.
created_at: 2025-02-16T16:46:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,246 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I am using it inside the deploy method. As part of the NNCP deploy method I also want to check that it was configured successfully.
created_at: 2025-02-16T16:57:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll remove it.
created_at: 2025-02-17T08:27:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Nice! Ack, thanks.
created_at: 2025-02-17T08:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-17T08:35:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Right! Ack, thanks.
created_at: 2025-02-17T08:36:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It is not called directly in the code - it is called by pytest since I'm using yield, which ensures cleanup after the test (and it is also triggered by the context manager).
created_at: 2025-02-17T08:49:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: The context manager triggers the clean_up, but if I want to explicitly delete the object I need to call delete() inside the clean_up method.
created_at: 2025-02-17T08:58:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Currently it is only used in one place, but it will prevent code duplication as the usage of this class will expand.
created_at: 2025-02-17T09:27:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: That a good point, I should check the live object instead.
created_at: 2025-02-17T09:41:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It is a valid scenario, for example when you only have bridge mapping for br-ex in a localnet scenario.
created_at: 2025-02-17T09:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
+        try:
+            return self.res["spec"]["desiredState"]["interfaces"]
+        except KeyError:
+            return []
+
+
+def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I'll refactor it into a staticmethod.
created_at: 2025-02-17T09:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: You can disable DHCP when creating an instance instead of making it the default in the class.
created_at: 2025-02-19T06:24:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: About the three variables, they’re only used once, so there's no strong reason to define them separately. From a readability perspective, the class-based references are already clear in context.
created_at: 2025-02-19T06:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Please add **kwargs as an argument to expose all inherited attributes, allowing the caller to set them. Make sure to pass it using super().__init__(**kwargs).
created_at: 2025-02-19T07:01:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: You can now base your class on:
https://github.com/RedHatQE/openshift-python-wrapper/blob/1b99a52bfe5e73e60505e4b55c850cb0e2a89f40/ocp_resources/node_network_configuration_policy_latest.py#L40
created_at: 2025-02-19T07:04:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT,
+        sleep=WAIT_FOR_STATUS_SLEEP,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for condition in nncp_conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == no_match_node_condition_reason
+                    or condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I don't think raising this exception is correct. If none of the conditions were met, it doesn't necessarily mean the NNCP failed to configure—it could still be in progress.
created_at: 2025-02-19T07:13:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: It is a must
created_at: 2025-02-19T07:48:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Nope, that is a very bad pattern and we should stop using it.
`kwargs`  has been badly used all over, it breaks API and violates basic OO principles.
created_at: 2025-02-19T07:50:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: @Anatw , you have not provided any reasoning to have variables like these here. This is what I am asking about, not about what they represent.

> About the three variables, they’re only used once, so there's no strong reason to define them separately.

"used once" is not a valid reason for not having a variable, function, class or whatever.

> From a readability perspective, the class-based references are already clear in context.

Exactly.

I expect the author to reason for them or remove them.
created_at: 2025-02-19T07:54:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: > It was added due to an error I got but now that I remove it - there are no complains. I ran it without the quotation marks and it runs fine. Thanks.

I think the question was about correctness and not about linter warnings.
IMO the fact the linter was quiet is not that convincing it is correct.

---

@yossisegev , based on [PEP-0484](https://peps.python.org/pep-0484/#forward-references):
> When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

So this is how it is done for these cases.
Once [PEP-794](https://peps.python.org/pep-0749/) lands, you'll be able and define the same without the quotes. 
created_at: 2025-02-19T08:32:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I guess you mean it is called by a parent class?
If that is the case, why you are not first doing the interface deleting and then calling the parent method?
I fail to understand why it needs to be re-implemented here.
created_at: 2025-02-19T08:34:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Related to prev thread: https://github.com/RedHatQE/openshift-virtualization-tests/pull/218/files#r1954511796
created_at: 2025-02-19T08:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: It may be correct, but it is hard to reason for it now.
Introduce it when it is needed so we can examine if it makes sense or not based on the usage.
created_at: 2025-02-19T08:36:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Interesting, I don't mind continue without it, but can you please give an example of how it can break API? 
created_at: 2025-02-19T09:52:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: 1. It is a "no limit" API, i.e. anything is accepted. When anything is accepted, anything can be ignored, blocked or treated differently. There is no ability to track it well, because it depends on how deep the inheritance is or how deep this is used in the object.
2. There is no proper API documentation.
3. It is impossible to provide recommendation to the users on how to use it. When the arguments are explicit, it not only comes with documentation, but it also comes with how the author intended it to be used.
4. In most cases I saw, it breaks the Interface Segregation Principle [1].

[1] https://en.wikipedia.org/wiki/Interface_segregation_principle
created_at: 2025-02-19T11:45:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Thanks!
@EdDev 
Noted.
created_at: 2025-02-19T11:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, thank you.
created_at: 2025-02-24T11:08:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I'll remove them, I agree it looks messy-er with the added variables.
created_at: 2025-02-24T11:12:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT,
+        sleep=WAIT_FOR_STATUS_SLEEP,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for condition in nncp_conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == no_match_node_condition_reason
+                    or condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: We will get here if we reached timeout - 90 seconds. In this stage it shouldn't be in progress.
created_at: 2025-02-24T11:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
+
+        return self.delete()
+
+    def interfaces(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll remove it.
created_at: 2025-02-24T11:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I mean that this function is called as part of the cleanup, and in this step (the cleanup) we need 2 things to happen:
1. Interfaces are set as absent and the NNCP is configured successfully.
2. The NNCP object is deleted.
Step 1 is the reason I need to overwrite the parent tear-down class.
I can change return self.delete() to return super().clean_up(), if this is what you mean.
created_at: 2025-02-24T11:39:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,163 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The wait_for_status_success function is not really waiting, because if in a case the expected status is not present, the function will raise NNCPConfigurationFailed which will stop the waiting functionality of TimeoutSampler.
You need to specify the group of exception that should be ignored and let the function managed by TimeoutSampler to continue using exceptions_dict in the decorator.
created_at: 2025-02-25T06:18:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT,
+        sleep=WAIT_FOR_STATUS_SLEEP,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for condition in nncp_conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == no_match_node_condition_reason
+                    or condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: No, it will get here after it will finish to iterate over all the conditions in self.instance.status.conditions.
created_at: 2025-02-25T06:22:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-25T12:21:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Shahaf, an you please mark this as resolved?
created_at: 2025-02-25T12:22:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT,
+        sleep=WAIT_FOR_STATUS_SLEEP,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for condition in nncp_conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == no_match_node_condition_reason
+                    or condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll refactor this function according to both your comments. Thanks you!
created_at: 2025-02-25T14:18:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,163 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Nice catch! Thank you, I'll add it.
created_at: 2025-02-25T14:19:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I don't have this option to resolve the thread.
created_at: 2025-02-26T08:41:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Only the author has it (crazy GitHub thing), so you can just express you are satisfy with a message or thumb-up and then the author can resolve it.
created_at: 2025-02-26T08:43:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ack, this thread can be resolved.
created_at: 2025-02-26T08:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Leftover.
created_at: 2025-02-26T13:38:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Suggestion:
Refactored to reduce nesting by filtering conditions upfront, improving readability and maintainability. This makes the logic clearer and avoids redundant checks.

```suggestion
    def wait_for_status_success(self) -> dict:
        conditions = [
            condition
            for condition in self.instance.status.conditions
            if condition["status"] == self.Condition.Status.TRUE
        ]
    
        for condition in conditions:
            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
                self.logger.info(f"{self.kind} {self.name} configured successfully")
                return condition
    
            if condition["reason"] in {
                self.Conditions.Reason.NO_MATCHING_NODE,
                self.Conditions.Reason.FAILED_TO_CONFIGURE,
            }:
                raise NNCPConfigurationFailed(
                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
                )
    
        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
```
created_at: 2025-02-26T13:48:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: I recommend not redefining the deploy method, as it complicates the context manager. The context manager should focus on deploying the resource, not waiting for the expected status. If we need to call __exit__ ourselves, we've likely done something wrong, as the context manager should handle that automatically. Instead, we can let the user call the wait logic separately if needed.
created_at: 2025-02-26T14:08:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ```suggestion
            if setattr(iface, "state", self.Interface.State.ABSENT) is None
```
created_at: 2025-02-26T14:24:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The is None check after setattr is redundant since setattr always returns None. Could you clarify its purpose? It can likely be removed for simplicity.
created_at: 2025-02-26T14:28:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +160,33 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=Resource.Interface.State.UP,
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: sbahar619
body: ```suggestion
                state=NetLibsNodeNetworkConfigurationPolicy.Interface.State.UP,
```
Then you can also remove the import of Resource.
created_at: 2025-02-26T15:05:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
+        ]
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: The same comment that I add for deploy:
I don't think we need to handle it with context manager, let the user handle it.
In the test fixture you can use the wait for success at setup, and delete interfaces for teardown.
created_at: 2025-02-26T17:39:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-27T09:45:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import re
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_enactment import NodeNetworkConfigurationEnactment
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import TimeoutExpiredError, TimeoutSampler
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IPv4:
+    auto_dns: bool = field(default=True)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class IPv6:
+    auto_dns: bool = field(default=True)
+    autoconf: bool = field(default=False)
+    dhcp: bool = field(default=False)
+    enabled: bool = field(default=False)
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    bridge: Bridge | None = None
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces] = field(default_factory=list)
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, Any] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - additional interfaces created.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Marking this as resolved.
created_at: 2025-02-27T09:54:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. I'm removing the entire deploy method, wait_for_status_success should be in the user's responsibility.
created_at: 2025-02-27T09:55:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Discussed offline. I'll split this list comprehension into 2 loops so we won't have the None which failed pre-commit. Marking as resolved.
created_at: 2025-02-27T11:52:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
+        ]
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Discussed offline. The wait will be removed only from deploy, but should be left as part of the cleanup since NNCP should be cleaned correctly and successfully before deleted. Marking as resolved.
created_at: 2025-02-27T11:54:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +160,33 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=Resource.Interface.State.UP,
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: Ack.
created_at: 2025-02-27T11:58:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-27T11:58:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-02-27T11:59:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Why you reference the API here?
Why dhcp specifically?
You should reference the whole API in the right place:
DesiredState

created_at: 2025-02-27T16:27:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Redundant, already defined by base class.
created_at: 2025-02-27T16:34:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: nit: Please express that it is in seconds.

I would also rename "sleep" to "interval" which gives it a bit more meaning.
created_at: 2025-02-28T06:56:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please arrange all classes here from top to bottom per their reference.
At the top, place `DesiredState` and from there per their order.

I will like to read this from top as a story.
created_at: 2025-02-28T07:00:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This can be a generator.
created_at: 2025-02-28T07:03:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Why newline in the end?
created_at: 2025-02-28T07:05:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I do not understand the need for this, can you please explain?
What is the added value to having this vs not having it (and returning None).
created_at: 2025-02-28T07:07:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This looks wrong.
I think we have already established that the reason is not the criteria to check, it is just the reasoning for why the condition has a specific status. The criteria for success is the relevant condition type and its status (TRUE).

For failures, you could filter by reason, but it is also not enough. You need to assure the type, the status (FALSE) and only then the reason.
created_at: 2025-02-28T07:16:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: At the moment you are not using the returned value anywhere.
Is there a reason to return a list of dicts?

I do not even understand how this is passing the typing linter. I do not see anything returning a list of dicts in the body.
created_at: 2025-02-28T07:21:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This should be "private".
created_at: 2025-02-28T07:27:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please order the methods for top to bottom such that the caller is before the callee.
created_at: 2025-02-28T07:29:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from kubernetes.dynamic import DynamicClient
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+from utilities.constants import TIMEOUT_5SEC, TIMEOUT_90SEC
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    auto_dns: bool = True
+    dhcp: bool = False
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interfaces:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interfaces]
+
+
+class NodeNetworkConfigurationPolicy(Resource):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Type:
+            DEGRADED = "Degraded"
+            AVAILABLE = "Available"
+
+        class Reason:
+            CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        client: DynamicClient | None = None,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+            client=client,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=TIMEOUT_90SEC,
+        sleep=TIMEOUT_5SEC,
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
+        no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
+        nncp_conditions = self.instance.status.conditions
+        for nncp_condition in nncp_conditions:
+            if nncp_condition["status"] == f"{True}":
+                if (
+                    nncp_condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED
+                    and nncp_condition["status"] == self.Condition.Status.TRUE
+                ):
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return nncp_condition
+
+                elif (
+                    nncp_condition["reason"] == no_match_node_condition_reason
+                    or nncp_condition["reason"] == failed_condition_reason
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure with error: {nncp_condition['reason']}\n"
+                    )
+
+        raise NNCPConfigurationFailed(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> NodeNetworkConfigurationPolicy:
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        if self._desired_state.interfaces:
+            for iface in self._desired_state.interfaces:
+                iface.state = Resource.Interface.State.ABSENT
+
+            desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+
+        ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: You can resolve this one, I see you called the parent method.
created_at: 2025-02-28T07:30:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,167 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            client (DynamicClient, optional): Optional DynamicClient to use.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        for condition in self.instance.status.conditions:
+            if condition["status"] == self.Condition.Status.TRUE:
+                if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                    self.logger.info(f"{self.kind} {self.name} configured successfully")
+                    return condition
+
+                if (
+                    condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                    or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+                ):
+                    raise NNCPConfigurationFailed(
+                        f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                    )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    def deploy(self, wait: bool = False) -> "NodeNetworkConfigurationPolicy":
+        self.create(wait=wait)
+        try:
+            self.wait_for_status_success()
+            return self
+        except Exception as exp:
+            self.logger.error(exp)
+            super().__exit__()
+            raise
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        desired_state["interfaces"] = [
+            asdict(iface)
+            for iface in self._desired_state.interfaces
+            if setattr(iface, "state", Resource.Interface.State.ABSENT) is None
+        ]
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: The NNCP cleanup must make sure all the parts that have been defined by it are teardown by it.
Trusting that the caller will do it is unsafe.
created_at: 2025-02-28T07:32:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I do not think you need this, you can fill it up directly. But if there is a problem to do it, just do this very close to the usage.
created_at: 2025-02-28T07:34:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        for iface in self._desired_state.interfaces:
+            iface.state = self.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This is overriding the desired state, which may be a problem.
You better do a deepcopy here.
created_at: 2025-02-28T07:38:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        for iface in self._desired_state.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Is this raising an exception?
If not, why you decided to "absorb" it?
What will happen if this fails to the rest of the tests?
created_at: 2025-02-28T07:42:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: The `NetLibs` prefix is not good and make no sense to me.

I see two options:
- Alias the import:
`from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as nncp`
- Call it `nncp`
created_at: 2025-02-28T07:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I'll change it to point to the main nmstate yaml api page.
created_at: 2025-03-03T08:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T08:47:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I can't  start with the **DesiredState** class since this class uses the **Interface** element, which should be declared beforehand, and so on with the Interface class content.
I'll change the order and I hope it will make a bit more sense.
created_at: 2025-03-03T12:07:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll go with your first suggestion. Unfortunately I'll have to use the ugly Nncp so that flake8 will be happy:
flake8...................................................................Failed
- hook id: flake8
- exit code: 1

libs/net/nodenetworkconfigurationpolicy.py:6:2: N817 camelcase 'NodeNetworkConfigurationPolicy' imported as acronym 'NNCP'

created_at: 2025-03-03T12:15:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T12:16:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I'm returning the condition, which is a list of dict.
For the moment it is indeed not in use.
I'll refactor it to simply return a bool True.
created_at: 2025-03-03T12:19:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Just for readability, it is more distinct. Not a must...
created_at: 2025-03-03T12:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T12:32:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T12:33:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        for iface in self._desired_state.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state["interfaces"] = [asdict(iface) for iface in self._desired_state.interfaces]
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    def clean_up(self) -> bool:
+        try:
+            self._delete_interfaces()
+            self.wait_for_status_success()
+        except Exception as exp:
+            self.logger.error(exp)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It logs the exception instead of raising it. You are raising a good point, I'll remove the try/except and will let it raise if needed.
created_at: 2025-03-03T12:50:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T13:10:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: This is meant to catch other NNCP failures. I don't mind removing it and if we see it is missing in the future it can be added.
created_at: 2025-03-03T14:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I changed the implementation to only iterate over the conditions with status True. From there we decide on success status based on the type (and not reason), and for failure we check the type and than the reason.
created_at: 2025-03-03T14:29:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. BTW, why isn't protected enough?
created_at: 2025-03-03T15:16:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    def _delete_interfaces(self) -> None:
+        desired_state = {}
+        for iface in self._desired_state.interfaces:
+            iface.state = self.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-03T15:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: This nmstate API specifications is only relevant for desiredState API, so you also need to move it there.
created_at: 2025-03-03T15:43:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: As mentioned in https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1977744062

This has nothing to do with this whole module, it is limited to the spec definition, which starts from the `DesiredState` definition. Add the docstring to it and provide some basic wording, a link is usually accompanied by some descriptive words.
created_at: 2025-03-03T19:02:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Ahh :/
Ok, so be it.
created_at: 2025-03-03T19:11:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: An error with newlines is strange, but I won't insist, it is a nit.
created_at: 2025-03-03T19:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: First time I hear about an exception that represents "all other" failures.
What is even more interesting here is that nothing is catch, i.e. no information, so I see no value in it.
created_at: 2025-03-03T19:24:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: There is no FAILING condition type for NNCP, drop it please.

This is a result of this messy inheritance which we should take care of as follow up.
created_at: 2025-03-03T19:55:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: @qinqon , once we determine the condition is of type `degraded` with status `true`, is there a need to check for the reason?
Degraded means it is a stable state of failure, right?
created_at: 2025-03-03T19:57:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: > I'm returning the condition, which is a list of dict.

AFAIK, a condition is a dict, not a list of anything.
The status conditions is a list of dicts.
But now it is gone, so it no longer matters.
created_at: 2025-03-03T20:00:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I meant one underscore and not name mangling.
Please set it to one underscore.
created_at: 2025-03-03T20:01:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: @myakove , how do you treat a teardown which may fail?
In this case, the teardown includes an intermediate step, but the question still stands for any other teardown (because the object deletion may timeout or be rejected).

I am guessing this is super rare and it will be noticeable due to the raised exception. However, I do not see what can be done here except maybe a retry or a stop to the test-run.
created_at: 2025-03-03T20:13:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: flake8 is wrong, but no point in arguing with a linter in this case.
created_at: 2025-03-03T20:15:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Ref for visibility: https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1974933672
created_at: 2025-03-03T20:16:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(dhcp=False),
+                ipv6=IPv6(dhcp=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
     ) as br_dev:
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: Am I missing something or this is an `nncp` and not a bridge device?
created_at: 2025-03-03T20:21:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(dhcp=False),
+                ipv6=IPv6(dhcp=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
     ) as br_dev:
+        br_dev.wait_for_status_success()
         yield br_dev
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    interface_name = net_add_op_bridge_device.instance["spec"]["desiredState"]["interfaces"][0]["name"]
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: 1. You do not need to re-read the nncp object. It is returned after it has been successfully deployed.
2. You should use a getter (as a property) to fetch the spec (`desired_state`) and access the relevant field.

E.g.
```python
interface_name = nncp_with_bridge.desired_state.interfaces[0].name
```
created_at: 2025-03-03T20:22:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: You can remove the double underscores (__) since they aren’t necessary here, and will prevent name mangling. A single underscore (_) will do the job for internal/private use.
created_at: 2025-03-04T05:09:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: You can remove all the logic behind node_selector and just pass it to the base class, it is already being handled there.
created_at: 2025-03-04T05:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: As I understand it correctly, if we end up with failure here, the object deletion is not being executed, is that intentional?
created_at: 2025-03-04T06:05:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ```suggestion
            if (condition["type"] in {Nncp.Condition.DEGRADED, Nncp.Condition.FAILING}) and (
                            condition["reason"]
                            in {
                                self.Conditions.Reason.NO_MATCHING_NODE,
                                self.Conditions.Reason.FAILED_TO_CONFIGURE,
                            }
                        ):
```
created_at: 2025-03-04T06:21:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: qinqon
body: You should check just Type, the Reason and Message are kind of informative and should "just" be shown as part of the error in the tests, from a user perspective the give them hints about what to fix, for example if NO_MATCHING_NODE maybe they use a bad nodeSelector or they deployed kubernetes-nmstate wrongly.

created_at: 2025-03-04T06:34:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Thank you!
created_at: 2025-03-04T06:52:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please read the thread above, this is irrelevant.
created_at: 2025-03-04T06:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Please look at the previous review rounds and the thread below that discusses this.
created_at: 2025-03-04T06:57:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: qinqon
body: > @qinqon , once we determine the condition is of type `degraded` with status `true`, is there a need to check for the reason? Degraded means it is a stable state of failure, right?

The kubernetes-nmstate implements rollback mechanism so network goes back to previews config, meaning that cluster should be stable network wise, if this is not happening then is a bug for kubernetes-nmstate or nmstate.
created_at: 2025-03-04T06:58:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: To be more precise, `to_dict` is not needed, you can pass the processed data to the parent.
created_at: 2025-03-04T07:43:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I thought that only double underscore is private, and single underscore is for protected methods. And I wasn't aware of this preventing the name mangling. Thank you, I'll change.
created_at: 2025-03-04T08:20:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-04T08:24:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: We don't like strange. I'll remove it.
created_at: 2025-03-04T08:35:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(dhcp=False),
+                ipv6=IPv6(dhcp=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
     ) as br_dev:
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: I left the original naming, I'll change.
created_at: 2025-03-04T08:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(dhcp=False),
+                ipv6=IPv6(dhcp=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
     ) as br_dev:
+        br_dev.wait_for_status_success()
         yield br_dev
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    interface_name = net_add_op_bridge_device.instance["spec"]["desiredState"]["interfaces"][0]["name"]
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: I was asked to remove it - https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1961206217
I'll add a getter.
created_at: 2025-03-04T08:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I need the to_dict to filter out None values. I'll move the desired_state and node_selector into the call to super().__init__().
created_at: 2025-03-04T09:18:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,159 @@
+# API: https://nmstate.io/devel/yaml_api.html#dhcp
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT = 90
+WAIT_FOR_STATUS_SLEEP = 5
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NetLibsNodeNetworkConfigurationPolicy(NodeNetworkConfigurationPolicy):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    api_group = Resource.ApiGroup.NMSTATE_IO
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT, sleep=WAIT_FOR_STATUS_SLEEP, exceptions_dict={NNCPSuccessStatusNotMet: []}
+    )
+    def wait_for_status_success(self) -> list[dict]:
+        conditions = [
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        ]
+
+        for condition in conditions:
+            if condition["reason"] == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return condition
+
+            if (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
+            ):
+                raise NNCPConfigurationFailed(
+                    f"{self.kind} {self.name} failed to configure on condition: {condition}\n"
+                )
+
+        raise NNCPSuccessStatusNotMet(f"{self.name}. None of the conditions were met.")
+
+    @staticmethod
+    def dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-04T09:19:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack, I'll change to a single underscore.
created_at: 2025-03-04T09:20:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    def _delete_interfaces(self) -> None:
+        desired_state_copy = deepcopy(self._desired_state)
+        for iface in desired_state_copy.interfaces:
+            iface.state = self.Interface.State.ABSENT
+        desired_state = {"interfaces": [asdict(iface) for iface in desired_state_copy.interfaces]}
+        if desired_state["interfaces"]:
+            ResourceEditor(patches={self: {"spec": {"desiredState": desired_state}}}).update()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if (condition["type"] == Nncp.Condition.DEGRADED or condition["type"] == Nncp.Condition.FAILING) and (
+                condition["reason"] == self.Conditions.Reason.NO_MATCHING_NODE
+                or condition["reason"] == self.Conditions.Reason.FAILED_TO_CONFIGURE
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: I'll remove the reasoning part.
created_at: 2025-03-04T09:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-04T10:55:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Make it plural:
ports
created_at: 2025-03-04T19:24:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: For consistency, please define the linter attribute with the same type annotation as in the base class: 
dict[str, Any] | None = None.
created_at: 2025-03-04T19:33:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Redundant, the base class already handle it.
created_at: 2025-03-04T19:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Please use Nncp.Condition.AVAILABLE and remove the Resource import. This avoids an additional import and keeps the code consistent.
created_at: 2025-03-04T19:39:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.kind} {self.name} failed to configure on condition:\n{condition}")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Nit: Consider formatting {self.kind} and {self.name} as {self.kind}/{self.name} to better match API conventions.
created_at: 2025-03-04T19:43:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Properties should ideally return attributes directly. Consider returning self.desired_state.interfaces as-is and moving asdict() elsewhere if needed.
created_at: 2025-03-04T19:48:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Let's be consistent with the other modules which just use SEC and not IN_SEC.
created_at: 2025-03-04T19:52:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: ```suggestion
    Represents the desired network configuration for Nmstate.
    following the Nmstate YAML-based API specification:
```
created_at: 2025-03-04T19:59:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: To avoid shared mutable state, consider using field(default_factory=list) for the port attribute.
created_at: 2025-03-04T20:22:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: NNCPSuccessStatusNotMet is redundant, it is not being raised anywhere.
created_at: 2025-03-04T20:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Not really being used.
created_at: 2025-03-04T20:27:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: This whole class is not used.
created_at: 2025-03-04T20:30:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: No need to pass desired_state to the base class, as it is already handled in the child class.
created_at: 2025-03-04T20:40:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Classification: nit
created_at: 2025-03-04T21:11:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: The spec [1] defines it like this.
It is singular.

[1] https://nmstate.io/examples.html#interface-linux-bridge
created_at: 2025-03-04T21:13:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: No, it is not a required field and should not take any such default.
The default is not to specify it and therefore it should stay `None`.
created_at: 2025-03-04T21:14:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Consistency?
Lets start with correctness.
This seems correct to me and is more accurate.
created_at: 2025-03-04T21:16:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This implementation is wrong, I will work on this with @Anatw offline.
created_at: 2025-03-04T21:17:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: This has been discussed offline: The lib object should let the exception raise and it will be up to the caller to do something special.

In general, there is nothing that can be done with this.
If it fails, we may leave leftovers.
To mitigate this, in a followup, we should randomize the names of the interfaces and the nncp itself, attempting to avoid a future collision even if something bad happened.
Said that, failure on edit or teardown is not seen on execution, so the priority to handle it is not high.
created_at: 2025-03-04T21:22:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.kind} {self.name} failed to configure on condition:\n{condition}")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: Why do we need the `kind` part? The fact it is an NNCP is embedded in the exception class. Most of the test is redundant really.
created_at: 2025-03-04T21:24:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Right, thanks, please ignore my comment then.
created_at: 2025-03-04T21:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: sbahar619
body: Right, thanks🙏🏼
created_at: 2025-03-05T06:05:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T08:45:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. I'll move what needed from here to the init function.
created_at: 2025-03-05T08:45:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+# API: https://nmstate.io/devel/yaml_api.html
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    interfaces: list[Interface]
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(
+            name=name,
+        )
+        self._desired_state = desired_state
+        self._node_selector = node_selector
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self._desired_state, dict_factory=self.__dict_normalization),
+                "nodeSelector": self._node_selector if self._node_selector else {},
+            })
+
+    @staticmethod
+    def __dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack. Thank you for looking into that.
created_at: 2025-03-05T08:47:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: It was needed so that the NNCPConfigurationFailed in the function will raise when seen instead of when the timout hit. I changed it into a general AssertionError and removed this exception class.
created_at: 2025-03-05T08:48:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T08:48:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
+        class Reason:
+            SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
+            FAILED_TO_CONFIGURE = "FailedToConfigure"
+            NO_MATCHING_NODE = "NoMatchingNode"
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        super().__init__(name=name, desired_state=desired_state, node_selector=node_selector)
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def interfaces(self) -> list[dict]:
+        return [asdict(interface) for interface in self.desired_state.interfaces]
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res.setdefault("spec", {}).update({
+                "desiredState": asdict(self.desired_state, dict_factory=self._dict_normalization),
+                "nodeSelector": self.node_selector if self.node_selector else {},
+            })
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_IN_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_IN_SEC,
+        exceptions_dict={NNCPSuccessStatusNotMet: []},
+    )
+    def wait_for_status_success(self) -> bool:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == self.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Resource.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind} {self.name} configured successfully")
+                return True
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.kind} {self.name} failed to configure on condition:\n{condition}")
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Discussed offline, I'm removing the kind from the NNCPConfigurationFailed string and shortening the string as well. We have redundant duplicated info.
created_at: 2025-03-05T08:49:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T10:03:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T10:18:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T10:18:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass, field
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import Resource, ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_IN_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_IN_SEC = 5
+
+
+@dataclass
+class IP:
+    dhcp: bool
+    auto_dns: bool = True
+    enabled: bool = False
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool = False
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4
+    ipv6: IPv6
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network state for Nmstate.
+    This class follows the Nmstate YAML API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface] = field(default_factory=list)
+
+
+class NNCPSuccessStatusNotMet(Exception):
+    pass
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    class Conditions:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Ack.
created_at: 2025-03-05T10:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: I'm not sure how I feel about this method... ;)
The caller is responsible for calling this class with valid keys and values, and if they don't do that - the attempt to create the resource should fail with the appropriate error (which kubernetes already does).
I don't think the resource class should cover-up for the caller sending invalid parameters.
created_at: 2025-03-05T13:24:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
+            iface["state"] = Nncp.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Maybe we should limit that only for specific interface types.
If the original NNCP is supposed to modify something in a node's interface, e.g. setting `eno3` to `down`, we wouldn't want this scenario to end with an attempt to delete `eno3`.
And more generally - we don't want to try deleting any interface that has always been there.
created_at: 2025-03-05T13:32:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: I do not understand your comment.
This method is used to drop `None` values and convert `_` field names to `-` in order to fit the use of `dataclass`.
basically, it is part of the mechanism to convert a structured data (`dataclass`) to a dict that is used to call the API.

It has nothing to do with the user calling anything. The user is building the structured data and if that is wrong the API will be the one who will fail it.
created_at: 2025-03-05T14:22:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
+            iface["state"] = Nncp.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: You are right, but this is following the current logic and going beyond that is a non-trivial follow-up.

In general, what you suggest here is possible.
Do we need it? Only time will tell (or you will tell us :) ).
created_at: 2025-03-05T14:27:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Adding to what Edi said, in the fields in dataclasses must have underscore in their names and can't have hyphens (e.g. allow_extra_patch_ports in  the Bridge dataclasses), while the ones in kubernetes resources , which follows yaml and json naming conventions, can have hyphens but not underscores. For this reason we must convert the fields.
created_at: 2025-03-05T15:10:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
+            iface["state"] = Nncp.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: After discussing it offline with @EdDev - I can live with this change as it is presented above for 2 reasons:
- It actually preserves the same logic we used until now, which haven't resulted with any attempt to delete any day-0 interface.
- Checking the interface type might not be enough, for example - the default `br-ex` is of type `ovs-bridge`, and we shouldn't attempt to remove it.
created_at: 2025-03-06T13:43:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: > in the fields in dataclasses must have underscore in their names and can't have hyphens

OK, that's the constraint I was not aware of.
Very well then, you can resolve this comment.
Thank you for the clarification.
created_at: 2025-03-06T13:46:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1983385143
Marking this conversation as resolved.
created_at: 2025-03-06T15:00:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
+            iface["state"] = Nncp.Interface.State.ABSENT
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Marking this conversation as resolved.
created_at: 2025-03-19T07:52:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: dbasunag
body: Can you please move this file, under tests/network? Then the root approvers should not have any objections to this PR.
created_at: 2025-04-08T13:50:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
path: libs/net/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: Sure, great new that we'll be able to move forward with this feature testing. Should it be a standalone? I think it better go into a new net-infra folder.
created_at: 2025-04-08T14:23:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: yossisegev
body: You are not configuring the NNCP desired-state here, but rather the NAD, so you cannot make any assumptions about the input NNCP, which means you cannot assume the bridge is in `interfaces[0]`.
You must iterate over all the `interfaces` and find the one you need (I think that looking for the one with `type: linux-bridge` is the correct thing to do, but you might come up with a more proper criteria).
created_at: 2025-04-09T09:24:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,16 @@
 
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: yossisegev
body: I think that it's better to separate the introduction of  the new resource (`libs/nodenetworkconfigurationpolicy.py) from its usage (`test_network_addons_operator.py`) to 2 separate PRs.
created_at: 2025-04-09T09:32:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Putting the resource here might be problematic: I think it violates the openshift-virtualization-tests repo rule of not importing from neighbor directories and packages.
For example: If we modify `network_device` utility to refer to this new resource (instead of the wrapper's resource), it will affect the code in `tests/virt`, that calls this utility.
Unless this resource is planned to be used only by network tests, and other components will continue to use the wrapper. If this is the case then pardon me for the noise, and please resolve this comment.
created_at: 2025-04-09T09:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: The NNCP might be one that modifies a node's native interfaces, for example - modify the MTU of eno9. In this case - you don't want to set it to `absent`.
created_at: 2025-04-09T09:57:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Possible code duplication: This is calling the wrapper's `clean_up`, which is also going in a flow of deleting interfaces by setting their status to `ABSENT`. 
created_at: 2025-04-09T10:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: My bad, I didn't look thoroughly enough and missed that this class inherits from the new wrapper's implementation of NNCP, where this clean-up flow is not implemented.
No duplication here, sorry for the noise.
created_at: 2025-04-09T11:21:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: dbasunag
body: If network team wants to use this code, then this needs to stay under tests/network. If you decide to modify anything in utility, then at that point, you would need to go by maintainers recommendations. This move is to help with getting this PR merged, as network team has been saying they are blocked without this PR.
created_at: 2025-04-09T11:44:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,16 @@
 
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: We should show the new code used in a test - otherwise it's not seen as needed.
For this reason, they must stay together.
created_at: 2025-04-09T11:52:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: This code in only for network team usage. Marking this comment as resolved.
created_at: 2025-04-09T11:54:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: This was already discussed. Here https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1981402126
created_at: 2025-04-09T12:07:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: The input spec for the NNCP is accessed, something defined at line 163 above.
If there will be a need for something more generic or if the definition will be far away from this access, then we will need to make it nicer, I agree. But for now, it seems good enough.
created_at: 2025-04-09T12:11:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: You raised it here some time ago: https://github.com/RedHatQE/openshift-virtualization-tests/pull/218#discussion_r1981402126

We agreed to take care of this once we will touch these interfaces as part of the iterative approach.

---
EDIT: What @Anatw said above.
created_at: 2025-04-09T12:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,16 @@
 
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: yossisegev
body: Accepted, please mark as resolved.

created_at: 2025-04-09T12:42:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: yossisegev
body: I strongly disagree, implementing this way is wrong.
And let's be honest - if we don't change it now, we will never change it, until it causes errors and failures. When this happens, it might take us some time to debug and find the root cause.
created_at: 2025-04-09T12:46:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
+        self.wait_for_status_success()
+        return super().clean_up()
+
+    @retry(
+        wait_timeout=WAIT_FOR_STATUS_TIMEOUT_SEC,
+        sleep=WAIT_FOR_STATUS_INTERVAL_SEC,
+        exceptions_dict={AssertionError: []},  # Using a no-op exception so any other error will be raised
+    )
+    def wait_for_status_success(self) -> dict[str, Any]:
+        conditions = (
+            condition
+            for condition in self.instance.status.conditions
+            if condition["status"] == Nncp.Condition.Status.TRUE
+        )
+
+        for condition in conditions:
+            if condition["type"] == Nncp.Condition.AVAILABLE:
+                self.logger.info(f"{self.kind}/{self.name} configured successfully")
+                return condition
+            if condition["type"] == Nncp.Condition.DEGRADED:
+                raise NNCPConfigurationFailed(f"{self.name} failed on condition:\n{condition}")
+        return {}
+
+    def _delete_interfaces(self) -> None:
+        desired_state = deepcopy(self.desired_state)
+        for iface in desired_state["interfaces"]:
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Very well, accepted.
Kudos for you guys for remembering my past comments :) .
Please resolve this conversation.
created_at: 2025-04-09T12:47:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: Well, I do not understand what you are protecting or how.
The object asked for is an nncp that defines a bridge and we know it is the only thing it defines because it is here a few lines above.

But if you have an elegant way to make it better, please provide the snipet and convince us it is safer/better. The only alternative I can think of is to know at the test itself the name of the bridge in advance and that can be done through a constant. In that case we will not need to query the nncp object for the name and will be a little uglier.
But there is no special benefit that I can see.

In any case, if your alternative is equivalent and not adding yet another redundant logic, I will be ok with it.
created_at: 2025-04-09T13:13:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: Ohh and knowing the bridge name in advance as a const is not only ugly, but probably wrong. It will not allow randomizing it as it should be.

And looking up for an interface of a specific type is not much better. Assuming you want to protect from a generic nncp spec (why?), then it will not work for cases of multiple such bridges.
created_at: 2025-04-09T13:19:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: yossisegev
body: This is what I am suggesting:
```
for interface in net_add_op_bridge_device.desired_state_spec.interfaces:
    if interface.type == "linux-bridge":
        bridge_name = interface.name
        break
```
or alternatively
```
bridge_name = next((interface.name for interface in net_add_op_bridge_device.desired_state_spec.interfaces if interface.type == "linux-bridge"), None)
```
And yes, I agree that this solution is also not 100% proof, in case the NNCP includes more than one `linux-bridge` interface, but the chances for that are much lower.
Anyway, if you live happily with the original implementation, then fine. This discussion has already occupied much more attention than what it is deserved.
@Anatw Please resolve this comment, regardless of what you and @EdDev decide .

created_at: 2025-04-09T13:32:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: EdDev
body: I’m fine with the `next` version, just drop the `None`.
created_at: 2025-04-09T13:52:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,21 +159,34 @@ def check_components(network_addons_config_scope_session):
 
 @pytest.fixture(scope="module")
 def net_add_op_bridge_device(worker_node1):
-    with utilities.network.network_device(
-        interface_type=LINUX_BRIDGE,
-        nncp_name="test-network-operator",
-        interface_name="net-add-br",
+    desired_state = DesiredState(
+        interfaces=[
+            Interface(
+                name="net-add-br",
+                state=NodeNetworkConfigurationPolicy.Interface.State.UP,
+                type=LINUX_BRIDGE,
+                ipv4=IPv4(enabled=False),
+                ipv6=IPv6(enabled=False),
+                bridge=Bridge(BridgeOptions(STP(enabled=False))),
+            )
+        ]
+    )
+    with NodeNetworkConfigurationPolicy(
+        name="test-network-operator",
+        desired_state=desired_state,
         node_selector=get_node_selector_dict(node_selector=worker_node1.hostname),
-    ) as br_dev:
-        yield br_dev
+    ) as nncp:
+        nncp.wait_for_status_success()
+        yield nncp
 
 
 @pytest.fixture(scope="module")
 def net_add_op_br1test_nad(namespace, net_add_op_bridge_device):
+    bridge_name = net_add_op_bridge_device.desired_state_spec.interfaces[0].name
path: tests/network/cluster_addons_operator/test_network_addons_operator.py
user: login: Anatw
body: Changed. marking this thread as resolved.
created_at: 2025-04-10T09:27:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: This clean-up flow raises back the risk of a race, which ends up with leftover interfaces on the node.
We must have the [same logic from the wrapper](https://github.com/RedHatQE/openshift-python-wrapper/blob/main/ocp_resources/node_network_configuration_policy.py#L326) here, i.e.
1. Check the timestamp of the NNCP's `AVAILABLE` status **before** updating the NNCP.
2. Update the NNCP with setting the interface state to `absent`.
3. Wait for an `AVAILABLE` status with a new timestamp.
created_at: 2025-04-10T10:24:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: Anatw
body: ACK. I'll address this in a follow up PR.
created_at: 2025-04-10T10:31:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: yossisegev
body: Acceptable.
But it should happen soon, so we don't get hit by the leftovers again, after finally getting rid of them.
created_at: 2025-04-10T10:45:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: rnetser
body: you should call `self.wait_for_configuration_conditions_unknown_or_progressing()` and not use timestamps.
was https://github.com/RedHatQE/openshift-python-wrapper/pull/2372 verified?

> ACK. I'll address this in a follow up PR.
this is new code that is being added, the comment should be addressed in this PR
created_at: 2025-04-10T11:42:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,140 @@
+from copy import deepcopy
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from ocp_resources.node_network_configuration_policy_latest import NodeNetworkConfigurationPolicy as Nncp
+from ocp_resources.resource import ResourceEditor
+from timeout_sampler import retry
+
+WAIT_FOR_STATUS_TIMEOUT_SEC = 90
+WAIT_FOR_STATUS_INTERVAL_SEC = 5
+
+
+@dataclass
+class IP:
+    enabled: bool
+    dhcp: bool | None = None
+    auto_dns: bool | None = None
+
+
+@dataclass
+class IPv4(IP):
+    pass
+
+
+@dataclass
+class IPv6(IP):
+    autoconf: bool | None = None
+
+
+@dataclass
+class STP:
+    enabled: bool
+
+
+@dataclass
+class BridgeOptions:
+    stp: STP
+
+
+@dataclass
+class Port:
+    name: str
+
+
+@dataclass
+class Bridge:
+    options: BridgeOptions | None = None
+    port: list[Port] | None = None
+    allow_extra_patch_ports: bool | None = None
+
+
+@dataclass
+class Interface:
+    name: str
+    type: str
+    state: str
+    ipv4: IPv4 | None = None
+    ipv6: IPv6 | None = None
+    bridge: Bridge | None = None
+
+
+@dataclass
+class DesiredState:
+    """
+    Represents the desired network configuration for NMstate.
+    Following the NMstate YAML-based API specification:
+    https://nmstate.io/devel/yaml_api.html
+    """
+
+    interfaces: list[Interface]
+
+
+class NodeNetworkConfigurationPolicy(Nncp):
+    """
+    NodeNetworkConfigurationPolicy object.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        desired_state: DesiredState,
+        node_selector: dict[str, str] | None = None,
+    ):
+        """
+        Create and manage NodeNetworkConfigurationPolicy
+
+        Args:
+            name (str): Name of the NodeNetworkConfigurationPolicy object.
+            desired_state (DesiredState): Desired policy configuration - interface creation, modification or removal.
+            node_selector (dict, optional): A node selector that specifies the nodes to apply the node network
+                configuration policy to.
+        """
+        self._desired_state = desired_state
+        super().__init__(
+            name=name,
+            desired_state=asdict(desired_state, dict_factory=self._dict_normalization),
+            node_selector=node_selector,
+        )
+
+    @staticmethod
+    def _dict_normalization(data: list[tuple[str, Any]]) -> dict[str, Any]:
+        """Filter out none values and converts key characters containing underscores into dashes."""
+        return {key.replace("_", "-"): val for (key, val) in data if val is not None}
+
+    @property
+    def desired_state_spec(self) -> DesiredState:
+        return self._desired_state
+
+    def clean_up(self) -> bool:
+        self._delete_interfaces()
path: tests/network/libs/nodenetworkconfigurationpolicy.py
user: login: EdDev
body: @rnetser , this is diverging to another big topic.
The timestamp is unavoidable because the behavior that was detected is that the condition read after the change is the one from before the change. The controller which is responsible to update it did not refresh its value. Therefore, the timestamp was used to confirm that the value is a fresh one.

Beyond that, the attempt to exist early on stable failure is already in place (at `wait_for_status_success`).
If we can discuss it in an immediate follow, it will be better.
created_at: 2025-04-10T12:01:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: ghost
created_at: 2025-02-03T09:03:40Z
body: 
Report bugs in [Issues](https://github.com/myakove/github-webhook-server/issues)

The following are automatically added:
 * Add reviewers from OWNER file (in the root of the repository) under reviewers section.
 * Set PR size label.
 * New issue is created for the PR. (Closed when PR is merged/closed)
 * Run [pre-commit](https://pre-commit.ci/) if `.pre-commit-config.yaml` exists in the repo.

Available user actions:
 * To mark PR as WIP comment `/wip` to the PR, To remove it from the PR comment `/wip cancel` to the PR.
 * To block merging of PR comment `/hold`, To un-block merging of PR comment `/hold cancel`.
 * To mark PR as verified comment `/verified` to the PR, to un-verify comment `/verified cancel` to the PR.
        verified label removed on each new commit push.
 * To cherry pick a merged PR comment `/cherry-pick <target branch to cherry-pick to>` in the PR.
    * Multiple target branches can be cherry-picked, separated by spaces. (`/cherry-pick branch1 branch2`)
    * Cherry-pick will be started when PR is merged
 * To build and push container image command `/build-and-push-container` in the PR (tag will be the PR number).
    * You can add extra args to the Podman build command
        * Example: `/build-and-push-container --build-arg OPENSHIFT_PYTHON_WRAPPER_COMMIT=<commit_hash>`
 * To add a label by comment use `/<label name>`, to remove, use `/<label name> cancel`
 * To assign reviewers based on OWNERS file use `/assign-reviewers`
 * To check if PR can be merged use `/check-can-merge`
 * to assign reviewer to PR use `/assign-reviewer @<reviewer>`

<details>
<summary>Supported /retest check runs</summary>

 * `/retest tox`: Retest tox
 * `/retest build-container`: Retest build-container
 * `/retest all`: Retest all

</details>

<details>
<summary>Supported labels</summary>

 * hold
 * verified
 * wip
 * lgtm

</details>
    
author_association: NONE
user: login: Anatw
created_at: 2025-02-03T14:02:11Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/e405d3e6e61fde236987317ee36299b99d49b7c5..a9449d527af9702eda18fcf8ffed99596cf2d496): refactor code according to CR:
 - When encounter NNCE error - log all errors and not just the first and last.
 - Rename  list to dict serialization function.
 - Remove parentheses from type return value.
 - Refactor NodeNetworkConfigurationPolicy docstring.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-03T14:04:47Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/0b5a42fa5c94d2c6fe8241b10a59c492744a6a62..49a0112a061c27ea7fedcbcdde0f366b290ba463): fix some pre-commit issues.
author_association: CONTRIBUTOR
user: login: sbahar619
created_at: 2025-02-10T13:12:10Z
body: Consider naming the file:
nodenetworkconfigurationpolicy.py ==> node_network_configuration_policy.py
For a better readability.
author_association: CONTRIBUTOR
user: login: dbasunag
created_at: 2025-02-11T22:30:33Z
body: /hold

copying over code from wrapper is not productive. If you don't like how the code is organized there, please create a PR there and work with the wrapper mantainers. I won't take this changes here.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-12T14:02:22Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/49a0112a061c27ea7fedcbcdde0f366b290ba463..779cf1f18a8c03e59edc922baed19b97b3871664):
 - Change the order of dataclasses for more readability.
 - Have a base IP class for use in IPv4 and IPv6.
 - In to_dict, if no node_selector was provided - set it to be an empty list.
 - In DesiredState, set interfaces to None instead of an empty list - the caller should specify that he want's an empty list.
 - Remove NNCE usage for error reason - must gather should take care of that, we only need to print the status 
    (success/failure).
 - In wait_for_status_success:
         - replace usage of TimeoutSampler with retry decorator.
         - Refine the logic  and only print the status (success/failure).
 - Rename _absent_interface to _delete_interfaces and work with the _desired_state instead of the dict.
 - Remove unused parameters from the clean_up signature.
 - Rename list_to_dict_serialize to dict_normalization and add a short description.
 - Remove redundant functions - _default_desired_ipv4 and _default_desired_ipv6.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-12T14:03:48Z
body: > Consider naming the file:
> nodenetworkconfigurationpolicy.py ==> node_network_configuration_policy.py
> For a better readability.
I agree it is more readable but I want it to be inline with the existing resource (netattachdef) + be clear that it is different from the wrapper node_network_configuration_policy.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-12T14:07:27Z
body: > Why would we want to duplicate wrapper code to the test repo? I am not sure I understand what you are trying to do.
> 
> /hold
> 
> copying over code from wrapper is not productive. If you don't like how the code is organized there, please create a PR there and work with the wrapper mantainers. I won't take this changes here.

Agreed, my purposes weren't clear enough, sorry for that.
Now the duplication is gone, and this new NNCP is more plain, focused on what we need starting from the base, what will allow us to more easily build upon it. And example for this you'll be able to see in my upcoming PR, adding localnet support to this NNCP, what we found really hard to do with the existing NNCP resource in the wrapper.
I'd appreciate if you can review it again and remove the hold if you agree.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-13T13:00:35Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/779cf1f18a8c03e59edc922baed19b97b3871664..8e3c3e4e07e7e2449cfadb3d0c6239fa56497edc):
 - Change the order of dataclasses and methods for better readability.
 - Remove leading underscore in _delete_interfaces.
 - Add try/except in interfaces
 - Change logs with "NNCP" to use self.kind.
 - In wait_for_status_success, check that the status is True before going over the conditions.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-17T10:39:44Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/8e3c3e4e07e7e2449cfadb3d0c6239fa56497edc..f4ea655b19305175a64ffad900570fae547d7443):
 - Replace generic timeout with specific one.
 - Rename Interfaces to Interface.
 - Remove class Type.
 - Removed CONFIGURATION_PROGRESSING Reason.
 - Remove client parameter from init.
 - Renamed some inner function variables.
 - In wait_for_status_success:
         - Use self.Condition.Status.TRUE.
         -  remove redundant  Status check.
         - Replace elif with if.
        - Raise the entire nncp_condition instead of just the reason.
 - In _delete_interfaces:
         - Remove redundant if self._desired_state.interfaces.
         - Convert 2 loops into one.
         - Run ResourceEditor only when needed.
 - In interfaces, check the live resource instead of res.
 - Refactor dict_normalization into a staticmethod.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-17T10:40:30Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/8e3c3e4e07e7e2449cfadb3d0c6239fa56497edc..f4ea655b19305175a64ffad900570fae547d7443): Remove #.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-24T11:54:27Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/9a7d2a2d9853bc094ea7732a6e60208c5153eec7..720462442bf935db1e4bcbfe06b502537c03ef1a):
 - Rename NodeNetworkConfigurationPolicy in net/libs to NetLibsNodeNetworkConfigurationPolicy.
 - NetLibsNodeNetworkConfigurationPolicy Now inherits from the NodeNetworkConfigurationPolicy in ocp_resources.node_network_configuration_policy_latest, instead of from Resource.
 - In wait_for_status_success, remove 3 variables (failed_condition_reason, no_match_node_condition_reason, nncp_conditions) and use their content directly.
 - Remove interfaces method and use it's content directly in net_add_op_br1test_nad.
 - In the clean_up method replace return self.delete() with return super().clean_up(). 
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-24T11:59:17Z
body: I forgot to push the changes on main independently, sorry.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-25T12:15:15Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-25T15:11:12Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/d51e2442d3b37341745ee3e515af47af70637a1d..a46f3067feecf4d746971031cfdf5a48fdfd0b36):
 - Add link to API.
In nodenetworkconfigurationpolicy:
 - Change in the IP class, dhcp has no default value.
 - Add a new costume exception - NNCPSuccessStatusNotMet
In wait_for_status_success:
 - In the retry decorator - add an exceptions_dict.
 - Instead of raising NNCPConfigurationFailed, raise a more specific raise - NNCPSuccessStatusNotMet (that was also added to the exceptions_dict).
In test_network_addons_operator:
 - In net_add_op_bridge_device, send dhcp=False in ipv4 and ipv6.
 - In net_add_op_br1test_nad, fix interface_name call.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-02-27T12:01:37Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: polarion-jenkins
created_at: 2025-02-27T12:05:32Z
body: D/S test `tox -e verify-tc-requirement-polarion` failed: cnv-tests-tox-executor/7383
author_association: COLLABORATOR
user: login: Anatw
created_at: 2025-02-27T13:27:49Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/c57245499f4656d4e7a87b120e3e644c6fbffdee..a4b02b7db0fd8351620a22bb6541f6ec2c744a37):
 - Remove redundant parameter in the docstring of NetLibsNodeNetworkConfigurationPolicy.
 - Refactor wait_for_status_success first for and if into a separate conditions list comprehension.
 - Remove the deploy method. Since we don't need to wait for status success as part of the deploy, but this should be waited by the user if needed, so there is no need to write over the super deploy.
 - In _delete_interfaces, refactor the single list comprehension into 2 loops, to avoid the use of "is None" that is failing pre-commit.
 - In net_add_op_bridge_device, replace usage of Resource to usage of NetLibsNodeNetworkConfigurationPolicy
 - In net_add_op_bridge_device, add a call to wait_for_status_success, which is now removed from the deploy method and is part of the user responsibility.
author_association: CONTRIBUTOR
user: login: polarion-jenkins
created_at: 2025-02-27T13:31:19Z
body: D/S test `tox -e verify-tc-requirement-polarion` failed: cnv-tests-tox-executor/7387
author_association: COLLABORATOR
user: login: Anatw
created_at: 2025-03-03T08:41:12Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-03T15:09:44Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-03T15:23:09Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/0f24256cf37265193774acda871ca7daba605bd0..fd8ecf132815420170cd5a111d94d8e62694fbeb):
 - Rename global variables.
 - Change the order of the dataclasses for better readability.
 - Rename NetLibsNodeNetworkConfigurationPolicy to NodeNetworkConfigurationPolicy (and change the import of the wrapper NodeNetworkConfigurationPolicy to nncp).
 - Refactor the wait_for_status_success method to rely on status and type before reason.
 - Inside class NodeNetworkConfigurationPolicy, change the order of functions so the caller is before the callee.
 - In _delete_interfaces remove desired_state initialization and assign directly.
 - In the clean_up, remove the try/catch redundant mechanism.
 - Set dict_normalization as a private method.
 - Add a deepcopy operation to the interfaces in _delete_interfaces.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-04T08:21:52Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-04T13:14:52Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/036b366d821056739d25d4fd26e098be1e3223d8..2d459d5affbd5b60296f9862c2b283c14d714f48):
 - Move API link into the DesiredState class docstring.
 - In the DesiredState class, change interfaces to hold an empty list by default.
 - In NodeNetworkConfigurationPolicy class, send desired_state and node_selector to super().__init__().
 - Add a property method interfaces getter.
 - Refactor __dict_normalization name to a single underscore.
 - Change the order of methods in the NodeNetworkConfigurationPolicy class for better readability.
 - In wait_for_status_success, remove any reference to reasons, we should only rely on the conditions' status and type.
 - In net_add_op_bridge_device, rename br_dev to nncp.
 - In net_add_op_br1test_nad, use the interfaces getter.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-04T15:48:58Z
body: Change: rebase on main.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-04T15:49:50Z
body: Change: fix pre-commit (add return value to wait_for_status_success).
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-05T11:31:37Z
body: [Changes](https://github.com/RedHatQE/openshift-virtualization-tests/compare/42085fb54ea15de9dd77d89f4cd63fa6aaa6e4c2..36bef94d6e3f354e5117ab01492fd993b4dbe2fa):
 - Rename global variables.
 - In dataclasses - set all non-requested values to default None.
 - Refactor DesiredState docstring.
 - Remove NNCPSuccessStatusNotMet.
 - In NodeNetworkConfigurationPolicy, remove class Conditions.
 - In NodeNetworkConfigurationPolicy, refactor init to send to super().init() the desired_data as dict and not as dataclass.
 - Remove the redundant to_dict method.
 - Remove the redundant interfaces method.
 - Add a desired_state_spec method.
 - In wait_for_status_success, replace the exceptions_dict exception to a general exception, only there so that any other error will be raised.
 - Replace self.Condition with Nncp.Condition.
 - Refactor logging in NodeNetworkConfigurationPolicy.
 - Remove duplication in _delete_interfaces.
 - Refactor test_network_addons_operator.py according to the changes.
author_association: CONTRIBUTOR
user: login: EdDev
created_at: 2025-03-05T14:35:37Z
body: @dbasunag , @myakove , this PR is ready.

I have discussed the topic of using objects that enhance the wrapper resources with @myakove .
We agreed that it is a good practice to innovate and evolve such abstractions on the tests repo, utilizing it for a period of time and wait for its adoption & stabilization before promoting it to the wrapper (which should be the end goal).

Therefore, I think this change is good to betaken into the repo, with a goal of gradually replacing the existing legacy NNCP object and once it reaches maturity, to promote it to the wrapper.

As with all the rest of the helpers under `/libs/net`, it will be owned by the sig-network.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-03-05T15:42:19Z
body: /build-and-push-container
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-03-05T15:43:06Z
body: New container for quay.io/openshift-cnv/openshift-virtualization-tests:pr-218 published
author_association: NONE
user: login: Anatw
created_at: 2025-03-06T11:53:58Z
body: /verified
iib: 927077
jenkins job: #1879
author_association: CONTRIBUTOR
user: login: myakove
created_at: 2025-03-06T12:24:23Z
body: > @dbasunag , @myakove , this PR is ready.
> 
> I have discussed the topic of using objects that enhance the wrapper resources with @myakove . We agreed that it is a good practice to innovate and evolve such abstractions on the tests repo, utilizing it for a period of time and wait for its adoption & stabilization before promoting it to the wrapper (which should be the end goal).
> 
> Therefore, I think this change is good to betaken into the repo, with a goal of gradually replacing the existing legacy NNCP object and once it reaches maturity, to promote it to the wrapper.
> 
> As with all the rest of the helpers under `/libs/net`, it will be owned by the sig-network.

Agreed, NNCP is problematic resource, make sure it's working as expected before pushing it to the wrapper is good idea.
About `/libs/net`  will be owned by the sig-network, I like it and I think the rest of the team should follow it.

author_association: COLLABORATOR
user: login: Anatw
created_at: 2025-04-08T16:58:12Z
body: Change: rebase on main
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-04-08T17:02:13Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/b25fa976d68aa2da098368c87c2d22d5a8f1a709..80648add3c46f3108e4c57163d5a7680b38f88cd): Moved the new nodenetworkconfigurationpolicy under network/libs.
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-04-08T17:07:30Z
body: /build-and-push-container
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-04-08T17:08:12Z
body: New container for quay.io/openshift-cnv/openshift-virtualization-tests:pr-218 published
author_association: NONE
user: login: Anatw
created_at: 2025-04-10T09:28:43Z
body: [Change](https://github.com/RedHatQE/openshift-virtualization-tests/compare/80648add3c46f3108e4c57163d5a7680b38f88cd..8b706c4fc3201e4c704464f5a61d7deebda1cb7f): address review comments (change getting the bridge name in the NAD from static to dynamic search).
author_association: CONTRIBUTOR
user: login: Anatw
created_at: 2025-04-10T09:29:25Z
body: /build-and-push-container
author_association: CONTRIBUTOR
user: login: ghost
created_at: 2025-04-10T09:30:07Z
body: New container for quay.io/openshift-cnv/openshift-virtualization-tests:pr-218 published
author_association: NONE
user: login: Anatw
created_at: 2025-04-10T10:29:38Z
body: /verified

Jenkins job: openshift-virtualization-tests-runner/2072
iib: 947266
cluster version: 4.19.0-ec.4
author_association: CONTRIBUTOR
user: login: vsibirsk
created_at: 2025-04-10T13:32:43Z
body: /hold cancel
author_association: COLLABORATOR
user: login: vsibirsk
created_at: 2025-04-10T13:34:49Z
body: /hold cancel
author_association: COLLABORATOR
user: login: vsibirsk
created_at: 2025-04-10T13:35:30Z
body: /check-can-merge
author_association: COLLABORATOR
user: login: ghost
created_at: 2025-04-10T13:41:40Z
body: Successfully removed PR tag: quay.io/openshift-cnv/openshift-virtualization-tests:pr-218.
author_association: NONE
user: login: ghost
created_at: 2025-04-10T13:43:35Z
body: New container for quay.io/openshift-cnv/openshift-virtualization-tests:latest published
author_association: NONE