Files
aci-sim/aci_sim/build/cabling.py
T
taodengandClaude Fable 5 7e9a175ce6 Initial public release — aci-sim v0.16.0
aci-sim is a stateful REST simulator of a 2-site Cisco ACI fabric
(per-site APIC + ND/NDO management planes) — for testing ACI automation
(Ansible cisco.aci / cisco.mso, aci-py, custom REST clients) and CI gates
without real hardware.

Highlights: APIC + NDO REST surface served from one in-memory MIT built
from a declarative topology.yaml; port mode + sandbox (real per-device IPs
on :443) run modes; LLDP/CDP neighbor visibility; NDO->APIC deploy mirror
(multi-site templates materialize onto the target sites' APIC stores);
real-APIC fvBD default attributes; file-backed state save/restore; an
`aci-sim` CLI (validate/show/graph/run/new/init/lldp); and an 888-test suite.

History squashed for the public release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:06:36 +10:00

95 lines
3.1 KiB
Python

"""build/cabling.py — fabricLink, lldpAdjEp, cdpAdjEp derived from the cabling graph.
fabricLink DN (verified against autoACI/backend/routers/topology.py lines 381-388):
topology/pod-{pod}/lnk-{n1}-{s1}-{p1}-to-{n2}-{s2}-{p2}
where "lnk-{n1}-{s1}-{p1}-to-...".split("-") = [n1, s1, p1, ...]
→ source_port = f"eth{s1}/{p1}" (spc[1], spc[2])
n1/n2 attributes carry the plain node IDs.
Port assignment (auto mode):
Spines: uplinks eth1/1, eth1/2, ... (one per downlink, in order)
Leaves/border-leaves: uplinks eth1/49, eth1/50, ... (one per spine, in order)
"""
from __future__ import annotations
from aci_sim.build.fabric import oob_ip
from aci_sim.build.neighbors import add_switch_adjacency
from aci_sim.mit.mo import MO
from aci_sim.mit.store import MITStore
from aci_sim.topology.schema import Node, Site, Topology
def cabling_links(site: Site) -> list[tuple[int, int, int, int, int, int]]:
"""Return (n1_id, slot1, port1, n2_id, slot2, port2) for every fabric link.
Convention: n1=spine, n2=leaf/border-leaf.
Spine port: eth1/{leaf_idx+1}
Leaf uplink: eth1/{49+spine_idx}
"""
spines = site.spine_nodes()
downlinks = site.leaf_nodes() + site.border_leaf_nodes()
links: list[tuple[int, int, int, int, int, int]] = []
for l_idx, leaf in enumerate(downlinks):
for s_idx, spine in enumerate(spines):
links.append((
spine.id, 1, l_idx + 1, # spine eth1/(leaf_idx+1)
leaf.id, 1, 49 + s_idx, # leaf eth1/(49+spine_idx)
))
return links
def build(topo: Topology, site: Site, store: MITStore) -> None:
"""Emit fabricLink + lldpAdjEp + cdpAdjEp derived from the cabling graph."""
pod = site.pod
# Build a lookup: node_id -> Node (for name/loopback lookups)
node_map: dict[int, Node] = {n.id: n for n in site.all_nodes()}
for n1, s1, p1, n2, s2, p2 in cabling_links(site):
lnk_dn = (
f"topology/pod-{pod}"
f"/lnk-{n1}-{s1}-{p1}-to-{n2}-{s2}-{p2}"
)
store.add(MO(
"fabricLink",
dn=lnk_dn,
n1=str(n1),
n2=str(n2),
operSt="up",
operSpeed="100G",
linkType="leaf",
))
# --- LLDP & CDP adjacencies on BOTH endpoints ---
node1 = node_map.get(n1)
node2 = node_map.get(n2)
if node1 is None or node2 is None:
continue
port1 = f"eth{s1}/{p1}"
port2 = f"eth{s2}/{p2}"
oob1 = oob_ip(pod, n1)
oob2 = oob_ip(pod, n2)
# Node1 sees Node2 as neighbor on port1 (Node2's own port is port2)
add_switch_adjacency(
store,
pod=pod,
local_node_id=n1,
local_port=port1,
remote_port=port2,
neighbor=node2,
neighbor_mgmt_ip=oob2,
)
# Node2 sees Node1 as neighbor on port2 (Node1's own port is port1)
add_switch_adjacency(
store,
pod=pod,
local_node_id=n2,
local_port=port2,
remote_port=port1,
neighbor=node1,
neighbor_mgmt_ip=oob1,
)