Commit 0588d203cf for frr

commit 0588d203cf88d6a1cf9b79621fc331ee38ccce78
Author: Donald Sharp <sharpd@nvidia.com>
Date:   Wed Sep 23 14:11:00 2026 -0400

    tests: Add a test to show TED pseudonodes are handled correctly

    Write a topotest that handles the introduction of ignoring
    the pseudonode LSP's in the TED.

    Signed-off-by: Donald Sharp <sharpd@nvidia.com>

diff --git a/tests/topotests/isis_te_pseudonode_topo1/r1/frr.conf b/tests/topotests/isis_te_pseudonode_topo1/r1/frr.conf
new file mode 100644
index 0000000000..356556f0cb
--- /dev/null
+++ b/tests/topotests/isis_te_pseudonode_topo1/r1/frr.conf
@@ -0,0 +1,28 @@
+hostname r1
+!
+interface lo
+ ip address 1.1.1.1/32
+ ip router isis 1
+ isis passive
+!
+interface eth-lan
+ ip address 10.0.1.1/24
+ ip router isis 1
+ isis hello-interval 1
+ isis hello-multiplier 3
+ link-params
+  enable
+  exit-link-params
+!
+router isis 1
+ net 49.0000.0000.0000.0001.00
+ is-type level-1
+ ! Long refresh so a periodic LSP refresh cannot repopulate the TED
+ ! and hide a pseudonode collision during the test.
+ lsp-timers gen-interval 1 refresh-interval 600 max-lifetime 1200
+ mpls-te on
+ mpls-te router-address 1.1.1.1
+!
+ip forwarding
+!
+end
diff --git a/tests/topotests/isis_te_pseudonode_topo1/r2/frr.conf b/tests/topotests/isis_te_pseudonode_topo1/r2/frr.conf
new file mode 100644
index 0000000000..8674f7191f
--- /dev/null
+++ b/tests/topotests/isis_te_pseudonode_topo1/r2/frr.conf
@@ -0,0 +1,38 @@
+hostname r2
+!
+interface lo
+ ip address 2.2.2.2/32
+ ip router isis 1
+ isis passive
+!
+interface eth-lan
+ ip address 10.0.1.2/24
+ ip router isis 1
+ isis hello-interval 1
+ isis hello-multiplier 3
+ ! Pin r2 as DIS on the LAN so its pseudonode LSP is in every LSDB.
+ isis priority 127
+ link-params
+  enable
+  exit-link-params
+!
+interface eth-p2p
+ ip address 10.0.2.2/24
+ ip router isis 1
+ isis network point-to-point
+ isis hello-interval 1
+ isis hello-multiplier 3
+ link-params
+  enable
+  exit-link-params
+!
+router isis 1
+ net 49.0000.0000.0000.0002.00
+ is-type level-1
+ lsp-timers gen-interval 1 refresh-interval 600 max-lifetime 1200
+ mpls-te on
+ mpls-te router-address 2.2.2.2
+!
+ip forwarding
+!
+end
diff --git a/tests/topotests/isis_te_pseudonode_topo1/r3/frr.conf b/tests/topotests/isis_te_pseudonode_topo1/r3/frr.conf
new file mode 100644
index 0000000000..17d8044664
--- /dev/null
+++ b/tests/topotests/isis_te_pseudonode_topo1/r3/frr.conf
@@ -0,0 +1,27 @@
+hostname r3
+!
+interface lo
+ ip address 3.3.3.3/32
+ ip router isis 1
+ isis passive
+!
+interface eth-p2p
+ ip address 10.0.2.3/24
+ ip router isis 1
+ isis network point-to-point
+ isis hello-interval 1
+ isis hello-multiplier 3
+ link-params
+  enable
+  exit-link-params
+!
+router isis 1
+ net 49.0000.0000.0000.0003.00
+ is-type level-1
+ lsp-timers gen-interval 1 refresh-interval 600 max-lifetime 1200
+ mpls-te on
+ mpls-te router-address 3.3.3.3
+!
+ip forwarding
+!
+end
diff --git a/tests/topotests/isis_te_pseudonode_topo1/test_isis_te_pseudonode_topo1.py b/tests/topotests/isis_te_pseudonode_topo1/test_isis_te_pseudonode_topo1.py
new file mode 100644
index 0000000000..4685cc8723
--- /dev/null
+++ b/tests/topotests/isis_te_pseudonode_topo1/test_isis_te_pseudonode_topo1.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2026 by Nvidia, Inc
+#                       Donald Sharp
+"""
+test_isis_te_pseudonode_topo1.py
+
+A LAN pseudonode LSP must not be applied to the real router's TED vertex.
+The TED is keyed by the 6-byte sysid, so isis_te_parse_lsp() and
+isis_te_delete_lsp() have to ignore r2.XX-00 instead of treating it as
+r2.00-00.
+
+    r1 ---- LAN 10.0.1.0/24 ---- r2 ---- P2P 10.0.2.0/24 ---- r3
+                                 DIS
+
+r2 is pinned as DIS and originates the only pseudonode. Its point-to-point
+adjacency to r3 is advertised with Local/Remote IP (10.0.2.2 -> 10.0.2.3).
+r3 is not on the LAN, so its reverse edge must survive both steps below;
+only r2's edge is exposed to the pseudonode.
+
+Setup conditions are waited on with run_and_expect(). The two checks that
+cover the bug itself are single-shot: polling them would let a later
+r2.00-00 refresh reparse the node LSP, restore the edge, and hide the
+collision.
+"""
+
+import os
+import re
+import sys
+from functools import partial
+
+import pytest
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+from lib import topotest
+from lib.topogen import Topogen, get_topogen
+from lib.topolog import logger
+
+pytestmark = [pytest.mark.isisd]
+
+R2_SYSID = "0000.0000.0002"
+R2_EDGE = "10.0.2.2"
+R2_EDGE_REMOTE = "10.0.2.3"
+R3_EDGE = "10.0.2.3"
+# Toggled to force r2 to reoriginate its node LSP without touching the LAN.
+_r2_loop_metric = {"value": 20}
+
+
+def build_topo(tgen):
+    "Build function"
+
+    for router in ["r1", "r2", "r3"]:
+        tgen.add_router(router)
+
+    lan = tgen.add_switch("s1")
+    lan.add_link(tgen.gears["r1"], nodeif="eth-lan")
+    lan.add_link(tgen.gears["r2"], nodeif="eth-lan")
+
+    p2p = tgen.add_switch("s2")
+    p2p.add_link(tgen.gears["r2"], nodeif="eth-p2p")
+    p2p.add_link(tgen.gears["r3"], nodeif="eth-p2p")
+
+
+def setup_module(module):
+    "Sets up the pytest environment"
+
+    tgen = Topogen(build_topo, module.__name__)
+    tgen.start_topology()
+
+    for router in tgen.routers().values():
+        router.load_frr_config()
+
+    tgen.start_router()
+
+
+def teardown_module():
+    "Teardown the pytest environment"
+    get_topogen().stop_topology()
+
+
+def vtysh_cfg(rname, *lines):
+    args = " ".join('-c "{}"'.format(line) for line in ("conf t",) + lines)
+    get_topogen().net[rname].cmd("vtysh {}".format(args))
+
+
+def router_out(rname, command):
+    return get_topogen().gears[rname].vtysh_cmd(command, isjson=False)
+
+
+def isis_state(rname):
+    return "{}\n{}".format(
+        router_out(rname, "show isis database"),
+        router_out(rname, "show isis mpls-te database"),
+    )
+
+
+def lsdb_has_pseudonode(rname, hostname, sysid):
+    "True when hostname/sysid has an LSP whose pseudonode id is not 00."
+    db = router_out(rname, "show isis database")
+    pattern = re.compile(
+        r"(?:^|\s)(?:{}|{})\.([0-9A-Fa-f]{{2}})-00(?:\s|$)".format(
+            re.escape(hostname), re.escape(sysid)
+        ),
+        re.M,
+    )
+    return any(pseudo != "00" for pseudo in pattern.findall(db))
+
+
+def lsdb_has_node_lsp(rname, hostname, sysid):
+    db = router_out(rname, "show isis database")
+    return "{}.00-00".format(hostname) in db or "{}.00-00".format(sysid) in db
+
+
+def ted_has_ipv4_edge(rname, local, remote):
+    """
+    True when the TED edge keyed by local has that remote address.
+
+    The brief edge line only prints the key. The remote address is under
+    "detail".
+    """
+    out = router_out(rname, "show isis mpls-te database edge {} detail".format(local))
+    if "No edge found" in out or "MPLS-TE is disabled" in out:
+        return False
+    return (
+        "Local IPv4 address: {}".format(local) in out
+        and "Remote IPv4 address: {}".format(remote) in out
+    )
+
+
+def ted_has_vertex(rname, name):
+    out = router_out(rname, "show isis mpls-te database vertex {}".format(name))
+    return "No vertex found" not in out and name in out
+
+
+def check_pseudonode_present(rname, hostname, sysid):
+    "None once rname's LSDB holds a pseudonode LSP from hostname/sysid."
+    if lsdb_has_pseudonode(rname, hostname, sysid):
+        return None
+    return router_out(rname, "show isis database")
+
+
+def check_pseudonode_gone(rname, hostname, sysid):
+    "None once hostname/sysid's pseudonode LSP has left rname's LSDB."
+    if not lsdb_has_pseudonode(rname, hostname, sysid):
+        return None
+    return router_out(rname, "show isis database")
+
+
+def check_ted_ipv4_edge(rname, local, remote):
+    "None once rname's TED holds the edge local -> remote."
+    if ted_has_ipv4_edge(rname, local, remote):
+        return None
+    return router_out(rname, "show isis mpls-te database detail")
+
+
+def force_r2_node_lsp_refresh():
+    """
+    Change r2's loopback metric so r2.00-00 is reoriginated. The resulting
+    LSP_UPD reparses only the node LSP, which puts the P2P edge back after
+    a pseudonode collision has removed it.
+    """
+    _r2_loop_metric["value"] = 50 if _r2_loop_metric["value"] == 20 else 20
+    vtysh_cfg("r2", "interface lo", "isis metric {}".format(_r2_loop_metric["value"]))
+
+
+def ensure_r2_is_dis_with_p2p_edge():
+    "r2 is DIS, and r1's TED has r2's point-to-point edge."
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    vtysh_cfg("r1", "interface eth-lan", "isis priority 64")
+    vtysh_cfg("r2", "interface eth-lan", "isis priority 127")
+
+    test_func = partial(check_pseudonode_present, "r1", "r2", R2_SYSID)
+    _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
+    assert (
+        result is None
+    ), "r2 never became DIS, no r2 pseudonode LSP in r1's LSDB:\n{}".format(result)
+
+    if not ted_has_ipv4_edge("r1", R2_EDGE, R2_EDGE_REMOTE):
+        logger.info("r2 P2P edge missing from r1 TED; refreshing r2.00-00")
+        force_r2_node_lsp_refresh()
+
+    test_func = partial(check_ted_ipv4_edge, "r1", R2_EDGE, R2_EDGE_REMOTE)
+    _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
+    assert result is None, "r1 TED never learned r2's P2P edge {} -> {}:\n{}".format(
+        R2_EDGE, R2_EDGE_REMOTE, result
+    )
+
+
+def rebuild_r1_ted():
+    "Drop and re-enable MPLS-TE so isis_te_init_ted() walks the whole LSDB."
+    vtysh_cfg("r1", "router isis 1", "no mpls-te")
+    vtysh_cfg(
+        "r1",
+        "router isis 1",
+        "mpls-te on",
+        "mpls-te router-address 1.1.1.1",
+    )
+
+
+def assert_r2_p2p_survives(reason):
+    """
+    Single-shot: run_and_expect() here would let the next r2.00-00 refresh
+    put the edge back before the loop gave up, hiding the bug.
+    """
+    edge = ted_has_ipv4_edge("r1", R2_EDGE, R2_EDGE_REMOTE)
+    vertex = ted_has_vertex("r1", "r2")
+
+    assert edge and vertex, (
+        "{}\n"
+        "r2 vertex in TED: {}\n"
+        "r2 P2P edge {} -> {} in TED: {}\n"
+        "r3 P2P edge {} -> {} in TED: {}\n"
+        "r2.00-00 still in LSDB: {}\n"
+        "{}".format(
+            reason,
+            vertex,
+            R2_EDGE,
+            R2_EDGE_REMOTE,
+            edge,
+            R3_EDGE,
+            R2_EDGE,
+            ted_has_ipv4_edge("r1", R3_EDGE, R2_EDGE),
+            lsdb_has_node_lsp("r1", "r2", R2_SYSID),
+            isis_state("r1"),
+        )
+    )
+
+
+def test_ted_rebuild_keeps_dis_p2p_edge():
+    """
+    Rebuilding the TED from an LSDB that contains the DIS pseudonode must
+    keep the DIS router's point-to-point edges.
+
+    isis_te_init_ted() walks the LSDB in LSP-ID order, so r2.00-00 is parsed
+    before r2.XX-00. Parsing the pseudonode finds that same vertex, marks its
+    edges ORPHAN, and ls_vertex_clean() deletes them: the pseudonode has no
+    Local/Remote IP sub-TLVs.
+    """
+    logger.info("TED rebuild must keep r2's P2P edge while r2's PSN is in the LSDB")
+    ensure_r2_is_dis_with_p2p_edge()
+    rebuild_r1_ted()
+    assert_r2_p2p_survives(
+        "rebuilding r1's TED from an LSDB that still contains r2's LAN "
+        "pseudonode dropped r2's point-to-point edge"
+    )
+
+
+def test_ted_keeps_dis_p2p_edge_after_pseudonode_purge():
+    """
+    Purging the DIS pseudonode must not delete the real router's TED vertex.
+
+    A receiver ages a purged pseudonode out after ZERO_AGE_LIFETIME (60s) and
+    lsp_destroy() sends LSP_DEL. isis_te_delete_lsp() copies only the 6-byte
+    sysid, so that delete removes r2.00-00's vertex and its outgoing edges.
+    The node LSP itself stays in the LSDB and is not reparsed.
+    """
+    logger.info("Purging r2's pseudonode must leave r2's TED vertex and P2P edge")
+    ensure_r2_is_dis_with_p2p_edge()
+
+    # Hand the LAN to r1. r2 resigns and purges r2.XX-00.
+    vtysh_cfg("r2", "interface eth-lan", "isis priority 0")
+    vtysh_cfg("r1", "interface eth-lan", "isis priority 127")
+
+    logger.info("waiting for r1 to age out r2's pseudonode LSP")
+    test_func = partial(check_pseudonode_gone, "r1", "r2", R2_SYSID)
+    _, result = topotest.run_and_expect(test_func, None, count=90, wait=1)
+    assert (
+        result is None
+    ), "r2's pseudonode LSP never aged out of r1's LSDB:\n{}".format(result)
+
+    assert_r2_p2p_survives(
+        "r1 deleted r2's TED vertex when r2's LAN pseudonode LSP aged out"
+    )