mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-19 09:46:33 +00:00
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>
95 lines
3.1 KiB
Python
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,
|
|
)
|