mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-21 21:15:12 +00:00
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>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""build/interfaces.py — l1PhysIf, ethpmPhysIf, eqptcapacityPolUsage5min per node.
|
||||
|
||||
Port layout:
|
||||
Fabric uplink ports: same assignment as cabling.py (derived independently)
|
||||
- Spines: eth1/1, eth1/2, ... (one per downlink node)
|
||||
- Leaves/BLs: eth1/49, eth1/50, ... (one per spine)
|
||||
Host-facing ports on leaves/border-leaves: eth1/1 – eth1/8 (simulated access)
|
||||
L3Out routed sub-interface port on border-leaves ONLY: eth1/48 (dedicated,
|
||||
outside the host-access range — matches real ACI convention of a dedicated
|
||||
front-panel port for the L3Out CSW uplink rather than sharing a host port).
|
||||
ISN/IPN uplink ports on spines ONLY, multi-site fabrics only: eth1/49, 50, ...
|
||||
(one per spine, dedicated — outside the fabric-uplink range eth1/1-4, which
|
||||
is fully consumed by intra-fabric spine↔leaf links). Real ACI spines facing
|
||||
a multi-site ISN use dedicated front-panel ports for the IPN uplink, distinct
|
||||
from the leaf-facing fabric ports; underlay.py's ospfAdjEp references these.
|
||||
|
||||
ethpmPhysIf DN must contain "phys-[eth{s}/{p}]" — read by topology.py line 730:
|
||||
port = dn.split("phys-[")[1].split("]")[0] → "eth1/1"
|
||||
|
||||
l1PhysIf DN is under the same sys/ subtree; id attribute carries the port name.
|
||||
|
||||
Batch-2 addition (CONTRACT.md §6 "Interfaces"): ethpmFcot — SFP/transceiver
|
||||
info, sibling of ethpmPhysIf under the same phys-[eth{s}/{p}] port DN (own RN
|
||||
"fcot", per real ACI's ethpmFcot placement one level under ethpmPhysIf's own
|
||||
"phys" RN's parent). Attrs verified read-only against autoACI's
|
||||
interface_comprehensive.py: typeName, guiName, vendorName, vendorSn,
|
||||
actualType (decoded via that plugin's own `_decode_sfp_field`/`_port_from_dn`
|
||||
helpers, which just strip a "phys-[" bracket segment off the dn — same
|
||||
placement l1PhysIf/ethpmPhysIf already use). Only emitted for fabric-uplink
|
||||
and L3Out routed ports (real optics-bearing ports); plain host-access ports
|
||||
are left without an ethpmFcot, matching real ACI labs where copper/DAC host
|
||||
ports often report no transceiver inventory while fabric/WAN-facing optical
|
||||
ports do — a deliberate realism choice, not an omission.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from aci_sim.mit.mo import MO
|
||||
from aci_sim.mit.store import MITStore
|
||||
from aci_sim.topology.schema import Site, Topology
|
||||
from aci_sim.build.cabling import cabling_links
|
||||
|
||||
_UPLINK_START = 49 # first leaf-side uplink port index
|
||||
_HOST_PORTS = 8 # number of host-facing ports on leaves
|
||||
_L3OUT_PORT = 48 # dedicated border-leaf L3Out routed sub-interface port
|
||||
|
||||
|
||||
def _add_port(
|
||||
store: MITStore,
|
||||
pod: int,
|
||||
node_id: int,
|
||||
slot: int,
|
||||
port: int,
|
||||
descr: str = "",
|
||||
mode: str = "trunk",
|
||||
usage: str = "fabric",
|
||||
with_fcot: bool = False,
|
||||
mtu: str = "9216",
|
||||
) -> None:
|
||||
"""Add l1PhysIf + ethpmPhysIf (+ optional ethpmFcot) for one port.
|
||||
|
||||
`mtu` defaults to the fabric intra-fabric MTU (9216); the ISN/IPN uplink
|
||||
port (multi-site only) passes `topo.isn.mtu` instead (Tier-2, PR-19) —
|
||||
real ACI's inter-pod/inter-site default is 9150, distinct from the
|
||||
intra-fabric 9216 ceiling.
|
||||
"""
|
||||
port_id = f"eth{slot}/{port}"
|
||||
sys_dn = f"topology/pod-{pod}/node-{node_id}/sys"
|
||||
phys_dn = f"{sys_dn}/phys-[{port_id}]"
|
||||
# l1PhysIf — administrative config
|
||||
store.add(MO(
|
||||
"l1PhysIf",
|
||||
dn=phys_dn,
|
||||
id=port_id,
|
||||
adminSt="up",
|
||||
descr=descr,
|
||||
mtu=mtu,
|
||||
mode=mode,
|
||||
))
|
||||
# ethpmPhysIf — operational state; DN must contain "phys-[eth…]"
|
||||
store.add(MO(
|
||||
"ethpmPhysIf",
|
||||
dn=f"{phys_dn}/phys",
|
||||
operSt="up",
|
||||
operSpeed="100G",
|
||||
usage=usage,
|
||||
lastLinkStChg="00:00:00:00.000",
|
||||
resetCtr="0",
|
||||
))
|
||||
# Batch-2: ethpmFcot — SFP/transceiver inventory, sibling of ethpmPhysIf
|
||||
# (own "fcot" RN under the same phys-[eth…] port DN — interface_comprehensive.py's
|
||||
# _port_from_dn strips the same "phys-[" bracket segment this DN shares with
|
||||
# ethpmPhysIf, so both resolve to the same port string). Only real
|
||||
# optics-bearing ports (fabric uplinks, ISN uplinks, L3Out routed ports)
|
||||
# get one — see module docstring for the realism rationale.
|
||||
if with_fcot:
|
||||
store.add(MO(
|
||||
"ethpmFcot",
|
||||
dn=f"{phys_dn}/fcot",
|
||||
typeName="QSFP-100G-SR4",
|
||||
guiName="QSFP-100G-SR4",
|
||||
vendorName="CISCO-FINISAR",
|
||||
vendorSn=f"FNS{pod:02d}{node_id:04d}{port:02d}",
|
||||
actualType="qsfp-100g-sr4",
|
||||
operSt="up",
|
||||
operSpeed="100G",
|
||||
))
|
||||
|
||||
|
||||
def build(topo: Topology, site: Site, store: MITStore) -> None:
|
||||
"""Emit l1PhysIf + ethpmPhysIf for all nodes, plus eqptcapacityPolUsage5min."""
|
||||
pod = site.pod
|
||||
spine_ids = {n.id for n in site.spine_nodes()}
|
||||
|
||||
# Collect port assignments from the cabling graph
|
||||
# spine_ports[node_id] = set of (slot, port) used as fabric uplinks
|
||||
# leaf_ports[node_id] = set of (slot, port) used as fabric uplinks
|
||||
spine_uplinks: dict[int, list[tuple[int, int]]] = {n.id: [] for n in site.spine_nodes()}
|
||||
leaf_uplinks: dict[int, list[tuple[int, int]]] = {
|
||||
n.id: [] for n in site.leaf_nodes() + site.border_leaf_nodes()
|
||||
}
|
||||
|
||||
for n1, s1, p1, n2, s2, p2 in cabling_links(site):
|
||||
if n1 in spine_ids:
|
||||
spine_uplinks[n1].append((s1, p1))
|
||||
leaf_uplinks[n2].append((s2, p2))
|
||||
else:
|
||||
spine_uplinks[n2].append((s2, p2))
|
||||
leaf_uplinks[n1].append((s1, p1))
|
||||
|
||||
# Spines — fabric uplinks, plus (multi-site only) dedicated ISN uplinks
|
||||
for si, spine in enumerate(site.spine_nodes()):
|
||||
for slot, port in spine_uplinks[spine.id]:
|
||||
_add_port(
|
||||
store, pod, spine.id, slot, port,
|
||||
descr=f"Fabric uplink eth{slot}/{port}",
|
||||
mode="trunk",
|
||||
with_fcot=True,
|
||||
)
|
||||
if len(topo.sites) > 1:
|
||||
isn_port = _UPLINK_START + si
|
||||
_add_port(
|
||||
store, pod, spine.id, 1, isn_port,
|
||||
descr=f"ISN/IPN uplink eth1/{isn_port}",
|
||||
mode="routed",
|
||||
usage="isn",
|
||||
with_fcot=True,
|
||||
mtu=str(topo.isn.mtu),
|
||||
)
|
||||
|
||||
border_leaf_ids = {bl.id for bl in site.border_leaves}
|
||||
|
||||
# Leaves + border-leaves — fabric uplinks + host-facing ports
|
||||
for leaf in site.leaf_nodes() + site.border_leaf_nodes():
|
||||
# Fabric uplink ports
|
||||
for slot, port in leaf_uplinks[leaf.id]:
|
||||
_add_port(
|
||||
store, pod, leaf.id, slot, port,
|
||||
descr="Fabric uplink to spine",
|
||||
mode="trunk",
|
||||
with_fcot=True,
|
||||
)
|
||||
# Host-facing access ports: eth1/1 … eth1/_HOST_PORTS
|
||||
for hp in range(1, _HOST_PORTS + 1):
|
||||
_add_port(
|
||||
store, pod, leaf.id, 1, hp,
|
||||
descr=f"Host port eth1/{hp}",
|
||||
mode="access",
|
||||
usage="access",
|
||||
)
|
||||
# Border-leaves only: dedicated L3Out routed sub-interface port,
|
||||
# outside the host-access range — l3out.py's l3extRsPathL3OutAtt
|
||||
# tDn references this exact port so it resolves against a real
|
||||
# l1PhysIf (real ACI convention: dedicated front-panel port for the
|
||||
# L3Out CSW uplink, not shared with regular host/EPG traffic).
|
||||
if leaf.id in border_leaf_ids:
|
||||
_add_port(
|
||||
store, pod, leaf.id, 1, _L3OUT_PORT,
|
||||
descr=f"L3Out routed uplink eth1/{_L3OUT_PORT}",
|
||||
mode="routed",
|
||||
usage="l3out",
|
||||
with_fcot=True,
|
||||
)
|
||||
# eqptcapacityPolUsage5min — capacity stats (contract-rendering metrics)
|
||||
store.add(MO(
|
||||
"eqptcapacityPolUsage5min",
|
||||
dn=f"topology/pod-{pod}/node-{leaf.id}/sys/eqptcapacity/polUsage5min",
|
||||
polUsage="42",
|
||||
polUsageCap="100",
|
||||
polUsageCum="38",
|
||||
polUsageCapCum="100",
|
||||
))
|
||||
Reference in New Issue
Block a user