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:
2026-07-05 20:06:36 +10:00
co-authored by Claude Fable 5
commit 7e9a175ce6
117 changed files with 31769 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
"""Entry point: python -m aci_sim.runtime.supervisor
Loads topology → build_all + build_ndo_model → two ApicSiteStates + NdoState
→ make_apic_app×2 + make_ndo_app → three uvicorn servers with TLS, gathered
with asyncio.
"""
from __future__ import annotations
import asyncio
import copy
import uvicorn
from aci_sim.build.orchestrator import build_all
from aci_sim.ndo.app import make_ndo_app
from aci_sim.ndo.model import build_ndo_model
from aci_sim.rest_aci.app import ApicSiteState, make_apic_app
from aci_sim.runtime.config import (
APIC_A_PORT,
APIC_B_PORT,
CERT_FILE,
KEY_FILE,
NDO_PORT,
SANDBOX,
SANDBOX_PORT,
SIM_BIND,
TOPOLOGY_PATH,
)
from aci_sim.topology.loader import load_topology
def apic_port_for_site(site_index: int) -> int:
"""Effective APIC port for a site, by index (#25).
- site 0 → APIC_A_PORT (the primary/first fabric's port env var)
- site 1 → APIC_B_PORT (so APIC_B_PORT, defined in config.py, is actually
honored instead of being silently shadowed by an APIC_A_PORT+i formula)
- site >1 → APIC_A_PORT + site_index, a documented fallback for topologies
with more than two sites (there is no APIC_C_PORT etc. env var; this
keeps ports contiguous and collision-free for the common 2-site case
while still supporting N sites).
"""
if site_index == 0:
return APIC_A_PORT
if site_index == 1:
return APIC_B_PORT
return APIC_A_PORT + site_index
async def run_servers() -> None:
topo = load_topology(TOPOLOGY_PATH)
stores = build_all(topo)
ndo_state = build_ndo_model(topo, sandbox=SANDBOX)
def _cfg(app, host, port):
return uvicorn.Config(
app, host=host, port=port,
ssl_certfile=CERT_FILE, ssl_keyfile=KEY_FILE, log_level="info",
)
# One APIC app per site (supports single-fabric or N sites).
# default → SIM_BIND (default 127.0.0.1, see #23) on distinct high ports (8443, 8444, …)
# sandbox → each site's own mgmt_ip on :443 (real-gear addressing, no ports)
configs = []
# site.id -> ApicSiteState, so the NDO app's deploy handler can mirror a
# deployed multi-site template's VRFs/BDs/ANPs/EPGs straight into the
# target sites' APIC MITStores (see ndo/deploy_mirror.py) — closing the
# confirmed SIM GAP where POST /mso/api/v1/task was a pure ack no-op and
# multi-site EPGs never appeared on the APIC side of the sim.
apic_states: dict = {}
for i, site in enumerate(topo.sites):
store = stores[site.name]
state = ApicSiteState(
name=site.name, site=site, topo=topo,
store=store, baseline=copy.deepcopy(store),
)
apic_states[site.id] = state
if SANDBOX:
host, port = (site.mgmt_ip or "127.0.0.1"), SANDBOX_PORT
else:
host, port = SIM_BIND, apic_port_for_site(i)
configs.append(_cfg(make_apic_app(state), host, port))
print(f"[sim] APIC {site.name} (asn {site.asn}) → https://{host if SANDBOX else SIM_BIND}:{port}")
if SANDBOX:
ndo_host, ndo_port = (topo.fabric.ndo_mgmt_ip or "127.0.0.1"), SANDBOX_PORT
else:
# NDO on NDO_PORT, shifted past the APIC ports if they would collide (>=3 sites).
ndo_host = SIM_BIND
last_apic = apic_port_for_site(len(topo.sites) - 1)
ndo_port = NDO_PORT if NDO_PORT > last_apic else last_apic + 1
configs.append(_cfg(make_ndo_app(ndo_state, apic_states), ndo_host, ndo_port))
print(f"[sim] NDO → https://{topo.fabric.ndo_mgmt_ip if SANDBOX else SIM_BIND}:{ndo_port}")
servers = [uvicorn.Server(cfg) for cfg in configs]
await asyncio.gather(*[s.serve() for s in servers])
def main() -> None:
asyncio.run(run_servers())
if __name__ == "__main__":
main()