Commit a703920aa9 for frr

commit a703920aa94ef8a19235e77b063d846990bcad02
Author: Chirag Shah <chirag@nvidia.com>
Date:   Sun Sep 13 18:40:36 2026 -0700

    lib: unconfigure user VRFs in lib_vrf_destroy without vrf_delete

    no vrf while the kernel device is up used to fail VALIDATE, so FRR kept
    inactive (configured) VRFs until a later reload. APPLY now clears
    VRF_CONFIGURED and skips vrf_delete() while VRF_ACTIVE so the RIB stays
    until netlink deletes the device. Keep the mgmtd abort coverage in
    test_regress2 via no interface on an active iface.

    Without this (current behavior):
    When no vrf is performed it would not remove "configured".

    until the net device is not deleted, the vrf2 stanza wil remained,
    only user configured flag is unset.

    no vrf vrf2
    show vrfs:
    vrf vrf1 id 78 table 1002 (configured)
    vrf vrf2 id 80 table 1004

    Ticket: #5233072

    Signed-off-by: Chirag Shah <chirag@nvidia.com>

diff --git a/lib/vrf.c b/lib/vrf.c
index b27866c976..30a2689d48 100644
--- a/lib/vrf.c
+++ b/lib/vrf.c
@@ -941,9 +941,13 @@ static int lib_vrf_destroy(struct nb_cb_destroy_args *args)
 	switch (args->event) {
 	case NB_EV_VALIDATE:
 		vrfp = nb_running_get_entry(args->dnode, NULL, true);
-		if (CHECK_FLAG(vrfp->status, VRF_ACTIVE)) {
+		/* Kernel-backed VRFs are unconfigured in APPLY, not rejected
+		 * here. Reject only the default VRF: it is always active and
+		 * vrf_delete() must not run against it.
+		 */
+		if (vrfp && strcmp(vrfp->name, VRF_DEFAULT_NAME) == 0) {
 			snprintf(args->errmsg, args->errmsg_len,
-				 "Only inactive VRFs can be deleted");
+				 "Default VRF cannot be deleted");
 			return NB_ERR_VALIDATION;
 		}
 		break;
@@ -952,9 +956,18 @@ static int lib_vrf_destroy(struct nb_cb_destroy_args *args)
 		break;
 	case NB_EV_APPLY:
 		vrfp = nb_running_unset_entry(args->dnode);
+		if (!vrfp)
+			return NB_OK;

-		/* Clear configured flag and invoke delete. */
 		UNSET_FLAG(vrfp->status, VRF_CONFIGURED);
+		/* vrf_delete() calls vrf_disable() when the VRF is enabled.
+		 * That must not run while the kernel device is still up:
+		 * drop FRR configuration only and leave the object in place.
+		 */
+		if (CHECK_FLAG(vrfp->status, VRF_ACTIVE) ||
+		    vrf_is_enabled(vrfp))
+			return NB_OK;
+
 		vrf_delete(vrfp);
 		break;
 	}
diff --git a/tests/topotests/mgmt_config/test_regress2.py b/tests/topotests/mgmt_config/test_regress2.py
index b24207d4a7..6422a05d2d 100644
--- a/tests/topotests/mgmt_config/test_regress2.py
+++ b/tests/topotests/mgmt_config/test_regress2.py
@@ -74,16 +74,41 @@ def test_regression_disconnect_after_abort(tgen):

     wl.snapshot()

-    step('Try to un-config an active vrf (i.e., "no vrf"), verify failure')
+    step('Un-config an active VRF ("no vrf"); kernel VRF stays, not configured')
     output = r1g.vtysh_multicmd(
         """
         show vrf
         conf t
             no vrf red
         end
+        show vrf
+        """
+    )
+    assert "Only inactive VRFs can be deleted" not in output
+    assert "Configuration failed" not in output
+    # Combined vtysh output includes both show vrf dumps; take the last
+    # "vrf red ..." status line (not the first, and not "no vrf red").
+    red_lines = [
+        line.strip()
+        for line in output.splitlines()
+        if line.strip().startswith("vrf red")
+    ]
+    # Both show vrf dumps must list kernel VRF "red" (created by the fixture).
+    assert len(red_lines) >= 2, output
+    # First dump is before no vrf; the stanza is still user-configured.
+    assert "(configured)" in red_lines[0], red_lines[0]
+    # Last dump is after no vrf; VRF_CONFIGURED is gone, kernel VRF remains.
+    assert "(configured)" not in red_lines[-1], red_lines[-1]
+
+    step("Trigger a VALIDATE abort via no interface on an active interface")
+    output = r1g.vtysh_multicmd(
+        """
+        conf t
+            no interface r1-eth0
+        end
         """
     )
-    assert "Only inactive VRFs can be deleted" in output
+    assert "only inactive interfaces can be deleted" in output.lower()

     logged = wl.snapshot()

@@ -95,8 +120,8 @@ def test_regression_disconnect_after_abort(tgen):
     regex = r"([-0-9A-Z_]+): \[[-0-9A-Z]*\] BE-CLIENT:.*Ignoring TXN_DELETE"
     matches = re.findall(regex, logged)
     assert (
-        len(matches) == 2
-    ), f"Wrong number of clients (2 != {len(matches)}) received TXN_REQ delete"
+        len(matches) >= 1
+    ), f"Wrong number of clients ({len(matches)}) received TXN_REQ delete"

     step("Checking for still locked regression")
     check_locked(r1g)