mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
feat(isn): VLAN-4 routed sub-interface + addr on spine ISN ospfIf; ISN-CSW LLDP neighbor (v0.19.0)
Real ACI multi-site spines carry the OSPF address toward the ISN on a
routed sub-interface, not the bare port: ospfIf id/dn are now
eth1/{49+si}.4 with addr=172.16.{site}.{si+1}/24 (same /24 as the
existing .254 peer); ospfAdjEp nests under the sub-if. Each spine ISN
uplink physical port also gets an ISN-CSW lldpAdjEp/cdpAdjEp
(sysName ISN-CSW{site}, portIdV Ethernet1/{si+1}, model N9K-C9364C,
locally-administered 02:1B:0D MAC hashed from site.id). Single-site
fabrics build none of this. Closes the autoACI Fabric (physical) gap
where spine uplink rows showed blank Local IP / Neighbor Port.
908 tests green.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
"""build/fabric.py — fabricNode, topSystem, fabricHealthTotal, vpcDom/vpcIf.
|
||||
|
||||
PR-22 addition: ISN CSW lldpAdjEp/cdpAdjEp — one per spine, multi-site only,
|
||||
on the spine's dedicated ISN uplink PHYSICAL port eth1/{49+si} (interfaces.py
|
||||
builds the l1PhysIf; underlay.py builds the OSPF sub-interface
|
||||
eth1/{49+si}.4 on the same base port). This is the third `add_adjacency`
|
||||
call site the module docstring in neighbors.py already accounts for
|
||||
(fabric.py) — it lives right after the existing APIC↔leaf adjacency loop in
|
||||
`build()` below, not a new module, per that docstring's warning against a
|
||||
fourth ad-hoc adjacency-construction site.
|
||||
|
||||
Batch-2 additions (CONTRACT.md §6 "Fabric/topology", "Infra write targets"; DN/
|
||||
attribute shapes verified read-only against autoACI's vpc_status.py/
|
||||
health_score.py — see docs/DESIGN.md "Batch-2" section):
|
||||
@@ -48,6 +57,8 @@ covers every ID our topology.yaml or a hand-authored one could reasonably use).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import zlib
|
||||
|
||||
from aci_sim.build.neighbors import add_adjacency, node_mac
|
||||
from aci_sim.mit.mo import MO
|
||||
from aci_sim.mit.store import MITStore
|
||||
@@ -259,6 +270,44 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
||||
neighbor_version=apic_version,
|
||||
)
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# itself even when a routed sub-if carries the IP, so this adjacency
|
||||
# deliberately targets the base port, not the sub-if). 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 in that view.
|
||||
if len(topo.sites) > 1:
|
||||
isn_ip = f"172.16.{site.id}.254"
|
||||
for si, spine in enumerate(site.spine_nodes()):
|
||||
add_adjacency(
|
||||
store,
|
||||
pod=pod,
|
||||
local_node_id=spine.id,
|
||||
local_port=f"eth1/{49 + si}",
|
||||
remote_port=f"Ethernet1/{si + 1}",
|
||||
neighbor_name=f"ISN-CSW{site.id}",
|
||||
neighbor_mgmt_ip=isn_ip,
|
||||
neighbor_kind="switch",
|
||||
neighbor_model="N9K-C9364C",
|
||||
neighbor_version="n9000-10.2(5)",
|
||||
# Synthetic chassis MAC for the (simulated, off-fabric) ISN
|
||||
# CSW device — deliberately a DIFFERENT OUI-shaped prefix
|
||||
# ("02:1B:0D", locally-administered per IEEE 802 bit-1-of-
|
||||
# first-octet convention) than node_mac()'s "00:1B:0D" used
|
||||
# for real fabric nodes, so this can never collide with a
|
||||
# spine/leaf/APIC chassis MAC regardless of site.id/node id
|
||||
# overlap. Keyed by site.id (not node id — the ISN CSW is
|
||||
# one device per site, not per spine). site.id is schema-typed
|
||||
# as an unconstrained str, so hash it instead of int()-casting
|
||||
# (a non-numeric id like "east" must not crash the build).
|
||||
neighbor_mac=(
|
||||
f"02:1B:0D:{zlib.crc32(str(site.id).encode()) & 0xFF:02X}:00:01"
|
||||
),
|
||||
)
|
||||
|
||||
# fabricHealthTotal — fabric-wide health summary
|
||||
store.add(MO("fabricHealthTotal", dn="topology/health", cur="100"))
|
||||
|
||||
|
||||
+22
-10
@@ -64,10 +64,18 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
||||
|
||||
# OSPF adjacencies — ONLY spine↔IPN/ISN, and ONLY in a multi-site fabric.
|
||||
# Leaves never run OSPF to the ISN. One adjacency per spine toward the IPN,
|
||||
# on eth1/{49+si} — interfaces.py builds a matching dedicated ISN uplink
|
||||
# l1PhysIf on each spine (multi-site only) at that exact port, so this
|
||||
# ospfAdjEp interface always resolves against a real port inventory entry.
|
||||
# autoACI reads: iface = dn.split("/if-[")[1]; neighbor = peerIp or nbrId.
|
||||
# modeled on a routed VLAN-4 SUB-INTERFACE of the dedicated ISN uplink port
|
||||
# eth1/{49+si} — i.e. "eth1/{49+si}.4" — matching real ACI multi-site
|
||||
# spines, which carry the ISN-facing OSPF address on a routed sub-if
|
||||
# rather than the bare physical port. interfaces.py builds the underlying
|
||||
# plain-port l1PhysIf/ethpmPhysIf on each spine (multi-site only) at that
|
||||
# exact base port (LLDP/physical state stays there — see interfaces.py's
|
||||
# module docstring); the ospfIf here is a logical child of that port, so
|
||||
# it always resolves against a real port inventory entry via its base
|
||||
# port (ifId.split('.')[0]).
|
||||
# autoACI's fabric_ospf plugin reads ospfIf.id/area/operSt directly; its
|
||||
# topology code matches ospfAdjEp by ifId and falls back to the base port
|
||||
# (ifId.split('.')[0]) to resolve the LLDP neighbor on the physical port.
|
||||
if len(topo.sites) > 1:
|
||||
ipn_ip = f"172.16.{site.id}.254" # the IPN/ISN router this site's spines peer with
|
||||
# Tier-2 (PR-19): isn.ospf_area feeds ospfIf.area/ospfAdjEp.area — was
|
||||
@@ -83,12 +91,15 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
||||
for si, spine in enumerate(site.spine_nodes()):
|
||||
ospf_base = f"topology/pod-{pod}/node-{spine.id}/sys/ospf/inst-default/dom-overlay-1"
|
||||
store.add(MO("ospfDom", dn=ospf_base, name="overlay-1", operSt="up"))
|
||||
# Batch-1: ospfIf nested between ospfDom and ospfAdjEp — same
|
||||
# eth1/{49+si} ISN uplink port interfaces.py already builds a
|
||||
# dedicated l1PhysIf for, so the existing ospfAdjEp below sits on
|
||||
# a real, matching ospfIf (consumer: autoACI's fabric_ospf.py,
|
||||
# which reads ospfIf.id (falling back to ifId) + area + operSt).
|
||||
if_port = f"eth1/{49 + si}"
|
||||
# Batch-1 (updated): ospfIf nested between ospfDom and ospfAdjEp
|
||||
# is now the routed VLAN-4 sub-interface of the eth1/{49+si} ISN
|
||||
# uplink port interfaces.py builds a dedicated l1PhysIf for — real
|
||||
# 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
|
||||
# /24 as the peer ipn_ip below so the adjacency stays
|
||||
# subnet-consistent (consumer: autoACI's fabric_ospf.py, which
|
||||
# reads ospfIf.id (falling back to ifId) + area + operSt).
|
||||
if_port = f"eth1/{49 + si}.4"
|
||||
if_dn = f"{ospf_base}/if-[{if_port}]"
|
||||
store.add(MO(
|
||||
"ospfIf",
|
||||
@@ -98,6 +109,7 @@ def build(topo: Topology, site: Site, store: MITStore) -> None:
|
||||
operSt="up",
|
||||
helloIntvl=ospf_hello,
|
||||
deadIntvl=ospf_dead,
|
||||
addr=f"172.16.{site.id}.{si + 1}/24",
|
||||
))
|
||||
store.add(MO(
|
||||
"ospfAdjEp",
|
||||
|
||||
Reference in New Issue
Block a user