mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
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:
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
||||||
(pre-1.0: minor bumps may include breaking changes to the sim's behavior).
|
(pre-1.0: minor bumps may include breaking changes to the sim's behavior).
|
||||||
|
|
||||||
|
## [0.19.1] - 2026-07-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **ISN sub-interface naming conventions flipped to match real gear** (owner
|
||||||
|
review of the v0.19.0 demo, verified against Cisco docs/real spine CLI):
|
||||||
|
the ACI spine names its VLAN-4 sub-if after the PORT (`eth1/49.49`,
|
||||||
|
cf. `Eth1/29.29`-style output in `show ip ospf neighbors vrf overlay-1`),
|
||||||
|
while the ISN Nexus names its dot1q-4 sub-if after the VLAN
|
||||||
|
(`Ethernet1/1.4`). v0.19.0 had it backwards (spine `.4`, CSW bare
|
||||||
|
physical): `ospfIf`/`ospfAdjEp` ids/dns now use the port-named form and
|
||||||
|
the ISN-CSW `lldpAdjEp.portIdV` gained the `.4` suffix.
|
||||||
|
|
||||||
## [0.19.0] - 2026-07-07
|
## [0.19.0] - 2026-07-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -273,12 +273,14 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
|||||||
# ISN CSW LLDP neighbor — multi-site only, one per spine's dedicated ISN
|
# ISN CSW LLDP neighbor — multi-site only, one per spine's dedicated ISN
|
||||||
# uplink PHYSICAL port (eth1/{49+si} — see interfaces.py/underlay.py: the
|
# uplink PHYSICAL port (eth1/{49+si} — see interfaces.py/underlay.py: the
|
||||||
# OSPF logical interface on this port is a routed VLAN-4 sub-interface,
|
# OSPF logical interface on this port is a routed VLAN-4 sub-interface,
|
||||||
# "eth1/{49+si}.4", but real LLDP is advertised on the physical port
|
# "eth1/{49+si}.{49+si}" per the ACI naming convention, but LLDP itself is
|
||||||
# itself even when a routed sub-if carries the IP, so this adjacency
|
# advertised per physical port, so this adjacency targets the base port).
|
||||||
# deliberately targets the base port, not the sub-if). autoACI's Topology
|
# The CSW-side port-id follows the IPN/ISN Nexus convention — a dot1q-4
|
||||||
# "Fabric (physical)" view derives this uplink row from ospfIf (port +
|
# routed sub-interface named after the VLAN ("Ethernet1/x.4", e.g. real
|
||||||
# local IP) + ospfAdjEp (remote IP) + this lldpAdjEp (CSW-side port), so
|
# IPN configs `interface Ethernet1/1.4` + `encapsulation dot1q 4`).
|
||||||
# without it the neighbor port/name show up blank in that view.
|
# autoACI's Topology "Fabric (physical)" view derives this uplink row from
|
||||||
|
# ospfIf (port + local IP) + ospfAdjEp (remote IP) + this lldpAdjEp
|
||||||
|
# (CSW-side port), so without it the neighbor port/name show up blank.
|
||||||
if len(topo.sites) > 1:
|
if len(topo.sites) > 1:
|
||||||
isn_ip = f"172.16.{site.id}.254"
|
isn_ip = f"172.16.{site.id}.254"
|
||||||
for si, spine in enumerate(site.spine_nodes()):
|
for si, spine in enumerate(site.spine_nodes()):
|
||||||
@@ -287,7 +289,7 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
|||||||
pod=pod,
|
pod=pod,
|
||||||
local_node_id=spine.id,
|
local_node_id=spine.id,
|
||||||
local_port=f"eth1/{49 + si}",
|
local_port=f"eth1/{49 + si}",
|
||||||
remote_port=f"Ethernet1/{si + 1}",
|
remote_port=f"Ethernet1/{si + 1}.4",
|
||||||
neighbor_name=f"ISN-CSW{site.id}",
|
neighbor_name=f"ISN-CSW{site.id}",
|
||||||
neighbor_mgmt_ip=isn_ip,
|
neighbor_mgmt_ip=isn_ip,
|
||||||
neighbor_kind="switch",
|
neighbor_kind="switch",
|
||||||
|
|||||||
@@ -95,11 +95,15 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
|||||||
# is now the routed VLAN-4 sub-interface of the eth1/{49+si} ISN
|
# is now the routed VLAN-4 sub-interface of the eth1/{49+si} ISN
|
||||||
# uplink port interfaces.py builds a dedicated l1PhysIf for — real
|
# uplink port interfaces.py builds a dedicated l1PhysIf for — real
|
||||||
# ACI multi-site spines carry the ISN-facing OSPF address on this
|
# ACI multi-site spines carry the ISN-facing OSPF address on this
|
||||||
# kind of sub-if, not the bare physical port. `addr` uses the same
|
# kind of sub-if, not the bare physical port. NAMING (verified
|
||||||
# /24 as the peer ipn_ip below so the adjacency stays
|
# against real spine CLI, e.g. "Eth1/29.29"/"Eth1/33.33" in
|
||||||
|
# `show ip ospf neighbors vrf overlay-1`): the ACI-side sub-if
|
||||||
|
# NUMBER equals the PORT number — the encap is always vlan-4 but
|
||||||
|
# only the IPN/ISN-router side names its sub-if ".4". `addr` uses
|
||||||
|
# the same /24 as the peer ipn_ip below so the adjacency stays
|
||||||
# subnet-consistent (consumer: autoACI's fabric_ospf.py, which
|
# subnet-consistent (consumer: autoACI's fabric_ospf.py, which
|
||||||
# reads ospfIf.id (falling back to ifId) + area + operSt).
|
# reads ospfIf.id (falling back to ifId) + area + operSt).
|
||||||
if_port = f"eth1/{49 + si}.4"
|
if_port = f"eth1/{49 + si}.{49 + si}"
|
||||||
if_dn = f"{ospf_base}/if-[{if_port}]"
|
if_dn = f"{ospf_base}/if-[{if_port}]"
|
||||||
store.add(MO(
|
store.add(MO(
|
||||||
"ospfIf",
|
"ospfIf",
|
||||||
|
|||||||
+1
-1
@@ -312,7 +312,7 @@ target-subtree-class=lldpAdjEp`) returned `totalCount=0` because the root DN had
|
|||||||
`--json` for machine-readable output).
|
`--json` for machine-readable output).
|
||||||
PR-19: `ospfIf.area`/`ospfAdjEp.area` are now `isn.ospf_area`-driven (default `"0.0.0.0"`) instead of a hardcoded literal.
|
PR-19: `ospfIf.area`/`ospfAdjEp.area` are now `isn.ospf_area`-driven (default `"0.0.0.0"`) instead of a hardcoded literal.
|
||||||
PR-20: `ospfIf.helloIntvl`/`deadIntvl` are new, driven by `isn.ospf_hello`/`isn.ospf_dead` (defaults 10/40, real ACI defaults) — previously not carried on this MO at all.
|
PR-20: `ospfIf.helloIntvl`/`deadIntvl` are new, driven by `isn.ospf_hello`/`isn.ospf_dead` (defaults 10/40, real ACI defaults) — previously not carried on this MO at all.
|
||||||
PR-22: the ISN-facing `ospfIf`/`ospfAdjEp` on each multi-site spine now sit on a routed VLAN-4 sub-interface (`id`/dn `eth1/{49+si}.4`, was the bare physical port `eth1/{49+si}`) and `ospfIf` gained an `addr` attribute (`172.16.{site.id}.{si+1}/24`) — matching real ACI multi-site spines, which carry the ISN OSPF address on a sub-if rather than the physical port. The underlying physical port's `l1PhysIf`/`ethpmPhysIf` (interfaces.py) are unchanged. Each spine's ISN uplink physical port also gained a simulated `lldpAdjEp`/`cdpAdjEp` (`sysName=ISN-CSW{site.id}`, `portIdV=Ethernet1/{si+1}`, model `N9K-C9364C`) via `build/fabric.py`, the third `add_adjacency` call site — see docs/DESIGN.md's "PR-22" section.
|
PR-22: the ISN-facing `ospfIf`/`ospfAdjEp` on each multi-site spine now sit on a routed VLAN-4 sub-interface (`id`/dn `eth1/{49+si}.{49+si}` — port-named per real ACI spine CLI (Eth1/29.29-style); was the bare physical port `eth1/{49+si}`) and `ospfIf` gained an `addr` attribute (`172.16.{site.id}.{si+1}/24`) — matching real ACI multi-site spines, which carry the ISN OSPF address on a sub-if rather than the physical port. The underlying physical port's `l1PhysIf`/`ethpmPhysIf` (interfaces.py) are unchanged. Each spine's ISN uplink physical port also gained a simulated `lldpAdjEp`/`cdpAdjEp` (`sysName=ISN-CSW{site.id}`, `portIdV=Ethernet1/{si+1}.4` (the ISN Nexus names its dot1q-4 sub-if after the VLAN), model `N9K-C9364C`) via `build/fabric.py`, the third `add_adjacency` call site — see docs/DESIGN.md's "PR-22" section.
|
||||||
|
|
||||||
**Overlay/BGP:** `bgpInst`(dn,asn) · `bgpPeer`(dn,asn,addr,type,peerRole) ·
|
**Overlay/BGP:** `bgpInst`(dn,asn) · `bgpPeer`(dn,asn,addr,type,peerRole) ·
|
||||||
`bgpPeerEntry`(dn,addr,operSt∈{established,...},rtrId,lastFlapTs,connEst,connDrop,type) ·
|
`bgpPeerEntry`(dn,addr,operSt∈{established,...},rtrId,lastFlapTs,connEst,connDrop,type) ·
|
||||||
|
|||||||
+4
-4
@@ -81,7 +81,7 @@ Each sub-builder is `build(topo, site, store)` and only ADDS MOs. `orchestrator.
|
|||||||
Sub-builders (one concern each, <~200 lines):
|
Sub-builders (one concern each, <~200 lines):
|
||||||
- `fabric.py` — fabricNode (roles/ids/model/serial/version from YAML), topSystem, fabricHealthTotal, vpcDom/vpcIf (border-leaf pair), infraWiNode.
|
- `fabric.py` — fabricNode (roles/ids/model/serial/version from YAML), topSystem, fabricHealthTotal, vpcDom/vpcIf (border-leaf pair), infraWiNode.
|
||||||
- `cabling.py` — fabricLink (with `/lnk-…/` DN), lldpAdjEp + cdpAdjEp derived STRICTLY from the cabling graph, isisAdjEp.
|
- `cabling.py` — fabricLink (with `/lnk-…/` DN), lldpAdjEp + cdpAdjEp derived STRICTLY from the cabling graph, isisAdjEp.
|
||||||
- `underlay.py` — bgpInst (local ASN), ospfAdjEp/**ospfIf**✅ (spine↔ISN, on the routed VLAN-4 sub-if `eth1/{49+si}.4` with `addr` — PR-22; multi-site only), IS-IS dom.
|
- `underlay.py` — bgpInst (local ASN), ospfAdjEp/**ospfIf**✅ (spine↔ISN, on the routed VLAN-4 sub-if `eth1/{49+si}.{49+si}` (port-named) with `addr` — PR-22; multi-site only), IS-IS dom.
|
||||||
- `overlay.py` — intra-site BGP-EVPN (spines as RR, leaves as clients): bgpPeer/bgpPeerEntry(established)/bgpPeerAfEntry;
|
- `overlay.py` — intra-site BGP-EVPN (spines as RR, leaves as clients): bgpPeer/bgpPeerEntry(established)/bgpPeerAfEntry;
|
||||||
**ISN**: on each spine's `sys/bgp/inst`, add inter-site bgpPeer with peer addr /32 + remote (other-site) ASN.
|
**ISN**: on each spine's `sys/bgp/inst`, add inter-site bgpPeer with peer addr /32 + remote (other-site) ASN.
|
||||||
- `endpoints.py` — fvCEp/fvIp distributed across leaves+EPGs (encap/fabricPathDn consistent), epmMacEp/epmIpEp, fvIfConn.
|
- `endpoints.py` — fvCEp/fvIp distributed across leaves+EPGs (encap/fabricPathDn consistent), epmMacEp/epmIpEp, fvIfConn.
|
||||||
@@ -835,15 +835,15 @@ ACI hardware is the authoritative backward-compat gate for this PR.
|
|||||||
modeled the ISN-facing `ospfIf` as a **plain interface** (`eth1/{49+si}`) with **no address**, and there
|
modeled the ISN-facing `ospfIf` as a **plain interface** (`eth1/{49+si}`) with **no address**, and there
|
||||||
was no LLDP neighbor on that port at all — so the demo UI showed `Port=eth1/49`, `Local IP=-`,
|
was no LLDP neighbor on that port at all — so the demo UI showed `Port=eth1/49`, `Local IP=-`,
|
||||||
`Neighbor Port=-`. Real ACI multi-site spines instead use a **routed sub-interface** (VLAN-4 encap, e.g.
|
`Neighbor Port=-`. Real ACI multi-site spines instead use a **routed sub-interface** (VLAN-4 encap, e.g.
|
||||||
`eth1/49.4`) that carries the OSPF address, while the ISN switch advertises LLDP on the underlying
|
`eth1/49.49`, port-named per real spine CLI) that carries the OSPF address, while the ISN switch advertises LLDP on the underlying
|
||||||
physical port. This PR makes the sim match that reality.
|
physical port. This PR makes the sim match that reality.
|
||||||
|
|
||||||
### `build/underlay.py` — `ospfIf` becomes a VLAN-4 routed sub-interface
|
### `build/underlay.py` — `ospfIf` becomes a VLAN-4 routed sub-interface
|
||||||
|
|
||||||
`ospfIf.id`/dn now read `eth1/{49+si}.4` (was `eth1/{49+si}`), and the MO gained a new `addr` attribute:
|
`ospfIf.id`/dn now read `eth1/{49+si}.{49+si}` (was `eth1/{49+si}`), and the MO gained a new `addr` attribute:
|
||||||
`172.16.{site.id}.{si+1}/24` — the same `/24` as the existing OSPF peer `172.16.{site.id}.254`, so the
|
`172.16.{site.id}.{si+1}/24` — the same `/24` as the existing OSPF peer `172.16.{site.id}.254`, so the
|
||||||
adjacency stays subnet-consistent. `area`/`helloIntvl`/`deadIntvl`/`operSt` are unchanged. `ospfAdjEp`'s
|
adjacency stays subnet-consistent. `area`/`helloIntvl`/`deadIntvl`/`operSt` are unchanged. `ospfAdjEp`'s
|
||||||
dn moves under the new sub-if segment (`if-[eth1/{49+si}.4]` instead of `if-[eth1/{49+si}]`); its
|
dn moves under the new sub-if segment (`if-[eth1/{49+si}.{49+si}]` instead of `if-[eth1/{49+si}]`); its
|
||||||
`peerIp`/`nbrId`/`operSt`/`area` attrs are unchanged. The underlying physical port
|
`peerIp`/`nbrId`/`operSt`/`area` attrs are unchanged. The underlying physical port
|
||||||
(`l1PhysIf`/`ethpmPhysIf`, built by `build/interfaces.py`) is **untouched** — only the OSPF logical
|
(`l1PhysIf`/`ethpmPhysIf`, built by `build/interfaces.py`) is **untouched** — only the OSPF logical
|
||||||
interface becomes a sub-interface; LLDP/physical port state stays on the plain port, matching real
|
interface becomes a sub-interface; LLDP/physical port state stays on the plain port, matching real
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "aci-sim"
|
name = "aci-sim"
|
||||||
version = "0.19.0"
|
version = "0.19.1"
|
||||||
description = "Faithful REST simulator of a 2-site Cisco ACI fabric (per-site APIC + ND/NDO)"
|
description = "Faithful REST simulator of a 2-site Cisco ACI fabric (per-site APIC + ND/NDO)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "PolyForm-Noncommercial-1.0.0"
|
license = "PolyForm-Noncommercial-1.0.0"
|
||||||
|
|||||||
@@ -91,8 +91,9 @@ class TestIsnDefaults:
|
|||||||
# PR-22 — ISN OSPF sub-interface + ISN CSW LLDP neighbor
|
# PR-22 — ISN OSPF sub-interface + ISN CSW LLDP neighbor
|
||||||
#
|
#
|
||||||
# Real ACI multi-site spines carry the ISN-facing OSPF address on a routed
|
# 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
|
# VLAN-4 sub-interface NAMED AFTER THE PORT (e.g. "eth1/49.49" — real spine
|
||||||
# the ISN switch advertises LLDP on that physical port. autoACI's Topology
|
# 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)
|
# "Fabric (physical)" view stitches these together: ospfIf (port + local IP)
|
||||||
# + ospfAdjEp (remote IP) + lldpAdjEp (CSW-side port/name).
|
# + ospfAdjEp (remote IP) + lldpAdjEp (CSW-side port/name).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -106,7 +107,7 @@ class TestIsnOspfSubInterfaceAndLldp:
|
|||||||
assert ospf_ifs, "expected at least one ospfIf"
|
assert ospf_ifs, "expected at least one ospfIf"
|
||||||
spine_ids = sorted(n.id for n in site.spine_nodes())
|
spine_ids = sorted(n.id for n in site.spine_nodes())
|
||||||
for si, sid in enumerate(spine_ids):
|
for si, sid in enumerate(spine_ids):
|
||||||
expected_id = f"eth1/{49 + si}.4"
|
expected_id = f"eth1/{49 + si}.{49 + si}"
|
||||||
mo = next(
|
mo = next(
|
||||||
m for m in ospf_ifs
|
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}]"
|
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)
|
store = orchestrator.build_site(repo_topo, site)
|
||||||
spine_ids = sorted(n.id for n in site.spine_nodes())
|
spine_ids = sorted(n.id for n in site.spine_nodes())
|
||||||
for si, sid in enumerate(spine_ids):
|
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(
|
adj = next(
|
||||||
m for m in store.by_class("ospfAdjEp")
|
m for m in store.by_class("ospfAdjEp")
|
||||||
if f"/node-{sid}/" in m.dn and expected_segment in m.dn
|
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"
|
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["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"
|
assert adj.attrs["mgmtIp"] == f"172.16.{site.id}.254"
|
||||||
|
|
||||||
def test_single_site_fabric_builds_no_isn_ospf_or_lldp(self) -> None:
|
def test_single_site_fabric_builds_no_isn_ospf_or_lldp(self) -> None:
|
||||||
|
|||||||
@@ -406,10 +406,11 @@ def test_ospf_adj_ep_sits_on_an_ospf_if(store_name, request):
|
|||||||
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
@pytest.mark.parametrize("store_name", _ALL_STORES)
|
||||||
def test_ospf_if_references_real_port(store_name, request):
|
def test_ospf_if_references_real_port(store_name, request):
|
||||||
"""ospfIf is now the routed VLAN-4 sub-interface of the spine's ISN
|
"""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
|
uplink port, named after the port per real ACI convention (e.g.
|
||||||
port state stays on the base port, so this resolves the underlying
|
"eth1/49.49" — cf. Eth1/29.29-style names in real spine CLI) — real
|
||||||
l1PhysIf via the base port (ifId.split('.')[0]), same fallback autoACI's
|
LLDP/physical port state stays on the base port, so this resolves the
|
||||||
topology code uses."""
|
underlying l1PhysIf via the base port (ifId.split('.')[0]), same
|
||||||
|
fallback autoACI's topology code uses."""
|
||||||
store = request.getfixturevalue(store_name)
|
store = request.getfixturevalue(store_name)
|
||||||
ports = _l1physif_ports(store)
|
ports = _l1physif_ports(store)
|
||||||
ifs = store.by_class("ospfIf")
|
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)
|
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}"
|
assert m is not None, f"unparsable ospfIf dn {ospf_if.dn!r}"
|
||||||
node_id, port_id = int(m.group(1)), m.group(2)
|
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, _, sub_num = port_id.partition(".")
|
||||||
base_port = port_id.split(".")[0]
|
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()), (
|
assert base_port in ports.get(node_id, set()), (
|
||||||
f"ospfIf {ospf_if.dn!r} references base port {base_port!r} on node "
|
f"ospfIf {ospf_if.dn!r} references base port {base_port!r} on node "
|
||||||
f"{node_id}, which has no matching l1PhysIf"
|
f"{node_id}, which has no matching l1PhysIf"
|
||||||
|
|||||||
@@ -127,9 +127,10 @@ def test_l3out_path_att_references_real_port(store_name, request):
|
|||||||
@pytest.mark.parametrize("store_name", ["store_a", "store_b"])
|
@pytest.mark.parametrize("store_name", ["store_a", "store_b"])
|
||||||
def test_ospf_adjacency_references_real_port(store_name, request):
|
def test_ospf_adjacency_references_real_port(store_name, request):
|
||||||
"""ospfAdjEp nests under the ospfIf's VLAN-4 sub-if segment
|
"""ospfAdjEp nests under the ospfIf's VLAN-4 sub-if segment
|
||||||
(if-[eth1/{49+si}.4]); resolve the underlying l1PhysIf via the base port
|
(if-[eth1/{49+si}.{49+si}], port-named per real ACI convention); resolve
|
||||||
(ifId.split('.')[0]) — same fallback autoACI's topology code uses since
|
the underlying l1PhysIf via the base port (ifId.split('.')[0]) — same
|
||||||
real LLDP/physical state stays on the base port, not the sub-if."""
|
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)
|
store = request.getfixturevalue(store_name)
|
||||||
ports = _l1physif_ports(store)
|
ports = _l1physif_ports(store)
|
||||||
adjs = list(store.by_class("ospfAdjEp"))
|
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)
|
m = re.search(r"/node-(\d+)/sys/ospf/.*?/if-\[([^\]]+)\]", adj.dn)
|
||||||
assert m is not None, f"unparsable ospfAdjEp dn {adj.dn!r}"
|
assert m is not None, f"unparsable ospfAdjEp dn {adj.dn!r}"
|
||||||
node_id, port_id = int(m.group(1)), m.group(2)
|
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, _, sub_num = port_id.partition(".")
|
||||||
base_port = port_id.split(".")[0]
|
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()), (
|
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"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)})"
|
f"which has no matching l1PhysIf (has {ports.get(node_id)})"
|
||||||
|
|||||||
Reference in New Issue
Block a user