fix(isn): flip sub-interface naming to real-gear conventions (v0.19.1)

Owner review of the v0.19.0 demo + Cisco docs/real spine CLI verification:
ACI spines name the VLAN-4 sub-if after the PORT (eth1/49.49, cf.
Eth1/29.29-style in show ip ospf neighbors vrf overlay-1); the ISN Nexus
names its dot1q-4 sub-if after the VLAN (Ethernet1/1.4). v0.19.0 had the
two conventions swapped. 908 tests green.
This commit is contained in:
dtzp555-max
2026-07-07 08:55:16 +10:00
parent f6c95a0105
commit c926702a29
9 changed files with 57 additions and 32 deletions
+6 -5
View File
@@ -91,8 +91,9 @@ class TestIsnDefaults:
# PR-22 — ISN OSPF sub-interface + ISN CSW LLDP neighbor
#
# Real ACI multi-site spines carry the ISN-facing OSPF address on a routed
# VLAN-4 sub-interface (e.g. "eth1/49.4"), not the bare physical port, and
# the ISN switch advertises LLDP on that physical port. autoACI's Topology
# VLAN-4 sub-interface NAMED AFTER THE PORT (e.g. "eth1/49.49" — real spine
# CLI shows Eth1/29.29-style names; only the ISN Nexus side names its dot1q-4
# sub-if ".4", e.g. "Ethernet1/1.4"), not the bare physical port. autoACI's Topology
# "Fabric (physical)" view stitches these together: ospfIf (port + local IP)
# + ospfAdjEp (remote IP) + lldpAdjEp (CSW-side port/name).
# ---------------------------------------------------------------------------
@@ -106,7 +107,7 @@ class TestIsnOspfSubInterfaceAndLldp:
assert ospf_ifs, "expected at least one ospfIf"
spine_ids = sorted(n.id for n in site.spine_nodes())
for si, sid in enumerate(spine_ids):
expected_id = f"eth1/{49 + si}.4"
expected_id = f"eth1/{49 + si}.{49 + si}"
mo = next(
m for m in ospf_ifs
if m.dn == f"topology/pod-{site.pod}/node-{sid}/sys/ospf/inst-default/dom-overlay-1/if-[{expected_id}]"
@@ -127,7 +128,7 @@ class TestIsnOspfSubInterfaceAndLldp:
store = orchestrator.build_site(repo_topo, site)
spine_ids = sorted(n.id for n in site.spine_nodes())
for si, sid in enumerate(spine_ids):
expected_segment = f"if-[eth1/{49 + si}.4]"
expected_segment = f"if-[eth1/{49 + si}.{49 + si}]"
adj = next(
m for m in store.by_class("ospfAdjEp")
if f"/node-{sid}/" in m.dn and expected_segment in m.dn
@@ -146,7 +147,7 @@ class TestIsnOspfSubInterfaceAndLldp:
if m.dn == f"topology/pod-{site.pod}/node-{sid}/sys/lldp/inst/if-[{local_port}]/adj-1"
)
assert adj.attrs["sysName"] == f"ISN-CSW{site.id}"
assert adj.attrs["portIdV"] == f"Ethernet1/{si + 1}"
assert adj.attrs["portIdV"] == f"Ethernet1/{si + 1}.4"
assert adj.attrs["mgmtIp"] == f"172.16.{site.id}.254"
def test_single_site_fabric_builds_no_isn_ospf_or_lldp(self) -> None:
+9 -6
View File
@@ -406,10 +406,11 @@ def test_ospf_adj_ep_sits_on_an_ospf_if(store_name, request):
@pytest.mark.parametrize("store_name", _ALL_STORES)
def test_ospf_if_references_real_port(store_name, request):
"""ospfIf is now the routed VLAN-4 sub-interface of the spine's ISN
uplink port (id/dn end in ".4", e.g. "eth1/49.4") — real LLDP/physical
port state stays on the base port, so this resolves the underlying
l1PhysIf via the base port (ifId.split('.')[0]), same fallback autoACI's
topology code uses."""
uplink port, named after the port per real ACI convention (e.g.
"eth1/49.49" — cf. Eth1/29.29-style names in real spine CLI) — real
LLDP/physical port state stays on the base port, so this resolves the
underlying l1PhysIf via the base port (ifId.split('.')[0]), same
fallback autoACI's topology code uses."""
store = request.getfixturevalue(store_name)
ports = _l1physif_ports(store)
ifs = store.by_class("ospfIf")
@@ -418,8 +419,10 @@ def test_ospf_if_references_real_port(store_name, request):
m = re.search(r"/node-(\d+)/sys/ospf/.*?/if-\[([^\]]+)\]", ospf_if.dn)
assert m is not None, f"unparsable ospfIf dn {ospf_if.dn!r}"
node_id, port_id = int(m.group(1)), m.group(2)
assert port_id.endswith(".4"), f"ospfIf {ospf_if.dn!r} id {port_id!r} is not a VLAN-4 sub-if"
base_port = port_id.split(".")[0]
base_port, _, sub_num = port_id.partition(".")
assert sub_num == base_port.split("/")[-1], (
f"ospfIf {ospf_if.dn!r} id {port_id!r} is not a port-named sub-if"
)
assert base_port in ports.get(node_id, set()), (
f"ospfIf {ospf_if.dn!r} references base port {base_port!r} on node "
f"{node_id}, which has no matching l1PhysIf"
+8 -5
View File
@@ -127,9 +127,10 @@ def test_l3out_path_att_references_real_port(store_name, request):
@pytest.mark.parametrize("store_name", ["store_a", "store_b"])
def test_ospf_adjacency_references_real_port(store_name, request):
"""ospfAdjEp nests under the ospfIf's VLAN-4 sub-if segment
(if-[eth1/{49+si}.4]); resolve the underlying l1PhysIf via the base port
(ifId.split('.')[0]) — same fallback autoACI's topology code uses since
real LLDP/physical state stays on the base port, not the sub-if."""
(if-[eth1/{49+si}.{49+si}], port-named per real ACI convention); resolve
the underlying l1PhysIf via the base port (ifId.split('.')[0]) — same
fallback autoACI's topology code uses since real LLDP/physical state
stays on the base port, not the sub-if."""
store = request.getfixturevalue(store_name)
ports = _l1physif_ports(store)
adjs = list(store.by_class("ospfAdjEp"))
@@ -138,8 +139,10 @@ def test_ospf_adjacency_references_real_port(store_name, request):
m = re.search(r"/node-(\d+)/sys/ospf/.*?/if-\[([^\]]+)\]", adj.dn)
assert m is not None, f"unparsable ospfAdjEp dn {adj.dn!r}"
node_id, port_id = int(m.group(1)), m.group(2)
assert port_id.endswith(".4"), f"ospfAdjEp {adj.dn!r} port {port_id!r} is not a VLAN-4 sub-if"
base_port = port_id.split(".")[0]
base_port, _, sub_num = port_id.partition(".")
assert sub_num == base_port.split("/")[-1], (
f"ospfAdjEp {adj.dn!r} port {port_id!r} is not a port-named sub-if"
)
assert base_port in ports.get(node_id, set()), (
f"ospfAdjEp {adj.dn!r} references base port {base_port!r} on node {node_id}, "
f"which has no matching l1PhysIf (has {ports.get(node_id)})"