mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-21 21:15:12 +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>
292 lines
12 KiB
Python
292 lines
12 KiB
Python
"""build/access.py — VLAN pools, physical/L3 domains, AAEPs, access port IPGs,
|
|
and (batch-1) the leaf interface-policy cluster: infraAccPortP/infraHPortS/
|
|
infraPortBlk/infraRsAccBaseGrp/infraAccBndlGrp.
|
|
|
|
Site-independent: call once per topology (site arg accepted for API uniformity
|
|
but not used in the selection logic).
|
|
|
|
Interface-policy cluster (CONTRACT.md §6 "Access policy") — real ACI DN/RN
|
|
conventions, verified against the existing infra tree this module already
|
|
builds (uni/infra/... prefix) and against autoACI's access_policy.py (read via
|
|
prior audit, see docs/CONTRACT.md header):
|
|
infraAccPortP uni/infra/accportprof-{name}
|
|
infraHPortS {accportp}/hports-{name}-typ-range
|
|
infraPortBlk {hports}/portblk-block{n}
|
|
infraRsAccBaseGrp {hports}/rsaccBaseGrp
|
|
infraAccBndlGrp uni/infra/funcprof/accbundle-{name}
|
|
|
|
Port ranges in infraPortBlk MUST match ports that actually exist in the
|
|
l1PhysIf inventory interfaces.py builds (eth1/1.._HOST_PORTS on leaves) — the
|
|
whole cluster is built once per border-leaf vPC pair (the topology's only vPC
|
|
domains, per fabric.py's vpcDom/vpcIf) plus one profile per regular-leaf pair,
|
|
so infraAccBndlGrp(lagT="node") lines up with a real vpcDom this same site
|
|
already has.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from aci_sim.mit.mo import MO
|
|
from aci_sim.mit.store import MITStore
|
|
from aci_sim.topology.schema import AAEP, L3Domain, PhysDomain, Site, Topology, VlanPool, VMMDomain
|
|
from aci_sim.build.interfaces import _HOST_PORTS
|
|
|
|
|
|
def _build_vlan_pool(pool: VlanPool) -> MO:
|
|
"""Build fvnsVlanInstP + fvnsEncapBlk child."""
|
|
pool_dn = f"uni/infra/vlanns-[{pool.name}]-static"
|
|
blk_dn = (
|
|
f"{pool_dn}"
|
|
f"/from-[vlan-{pool.from_vlan}]-to-[vlan-{pool.to_vlan}]"
|
|
)
|
|
pool_mo = MO("fvnsVlanInstP", dn=pool_dn, name=pool.name, allocMode="static")
|
|
blk_mo = MO(
|
|
"fvnsEncapBlk",
|
|
dn=blk_dn,
|
|
allocMode="static",
|
|
**{"from": f"vlan-{pool.from_vlan}", "to": f"vlan-{pool.to_vlan}"},
|
|
)
|
|
pool_mo.add_child(blk_mo)
|
|
return pool_mo
|
|
|
|
|
|
def _build_phys_dom(phys_dom: PhysDomain, first_pool_dn: str) -> MO:
|
|
"""Build physDomP + optional infraRsVlanNs child."""
|
|
dom_mo = MO("physDomP", dn=f"uni/phys-{phys_dom.name}", name=phys_dom.name)
|
|
if first_pool_dn:
|
|
dom_mo.add_child(MO(
|
|
"infraRsVlanNs",
|
|
dn=f"uni/phys-{phys_dom.name}/rsvlanNs",
|
|
tDn=first_pool_dn,
|
|
))
|
|
return dom_mo
|
|
|
|
|
|
def _build_l3_dom(l3_dom: L3Domain, first_pool_dn: str) -> MO:
|
|
"""Build l3extDomP + optional infraRsVlanNs child."""
|
|
l3dom_mo = MO("l3extDomP", dn=f"uni/l3dom-{l3_dom.name}", name=l3_dom.name)
|
|
if first_pool_dn:
|
|
l3dom_mo.add_child(MO(
|
|
"infraRsVlanNs",
|
|
dn=f"uni/l3dom-{l3_dom.name}/rsvlanNs",
|
|
tDn=first_pool_dn,
|
|
))
|
|
return l3dom_mo
|
|
|
|
|
|
def _build_vmm_domain(vmm: VMMDomain, first_pool_dn: str) -> MO:
|
|
"""Build vmmDomP (+ vmmCtrlrP + vmmUsrAccP if vcenter_ip set, + infraRsVlanNs).
|
|
|
|
Tier-2 (PR-19). Real ACI DN scheme (verified against aci-py's
|
|
`_domain_dn_class` shim — see docs/DESIGN.md PR-19 note):
|
|
vmmDomP uni/vmmp-VMware/dom-{name}
|
|
vmmCtrlrP {vmmDomP dn}/ctrlr-{name} hostOrIp=vcenter_ip, rootContName=datacenter
|
|
vmmUsrAccP {vmmDomP dn}/usracc-{name} placeholder credential-profile ref (no
|
|
real credentials in a sim — name-only, matches the pattern
|
|
phys/l3 domains use for their own no-secret child objects)
|
|
|
|
Only "VMware" (vmmProvP=VMware) is modeled — the sole provider the
|
|
aci-py `bind_epg_to_vmm_domain` playbook chain (and the DN map in
|
|
`mso_schema_site_anp_epg_domain.py`, `dn_map["vmmDomain"] =
|
|
"uni/vmmp-VMware/dom-{0}"`) actually exercises today.
|
|
|
|
vmmCtrlrP/vmmUsrAccP are only emitted when `vcenter_ip` is set — a bare
|
|
`{name: ...}` VMM domain entry (no vCenter attrs) produces just the
|
|
vmmDomP + optional infraRsVlanNs, same shape as a phys/l3 domain with no
|
|
extra config, so a topology author who only wants a named domain isn't
|
|
forced to supply vCenter connection details.
|
|
"""
|
|
dom_dn = f"uni/vmmp-VMware/dom-{vmm.name}"
|
|
dom_mo = MO("vmmDomP", dn=dom_dn, name=vmm.name)
|
|
if vmm.vcenter_ip:
|
|
ctrlr_dn = f"{dom_dn}/ctrlr-{vmm.name}"
|
|
ctrlr_mo = MO(
|
|
"vmmCtrlrP",
|
|
dn=ctrlr_dn,
|
|
name=vmm.name,
|
|
hostOrIp=vmm.vcenter_ip,
|
|
rootContName=vmm.datacenter or vmm.name,
|
|
dvsVersion="unmanaged",
|
|
)
|
|
if vmm.vcenter_dvs:
|
|
ctrlr_mo.attrs["dvsName"] = vmm.vcenter_dvs
|
|
dom_mo.add_child(ctrlr_mo)
|
|
dom_mo.add_child(MO(
|
|
"vmmUsrAccP",
|
|
dn=f"{ctrlr_dn}/usracc-{vmm.name}",
|
|
name=vmm.name,
|
|
))
|
|
if vmm.vlan_pool and first_pool_dn:
|
|
# first_pool_dn is topo.access.vlan_pools[0] (the same convention
|
|
# phys/l3 domains use below); Tier-2 vmm.vlan_pool is validated
|
|
# against the real pool-name set in schema.py's normalize_and_validate
|
|
# but this builder still binds the module-wide first pool DN (the
|
|
# existing phys/l3-domain convention), matching a dynamic VMM pool
|
|
# binding by name rather than resolving a distinct pool DN per VMM
|
|
# domain — sufficient for the sim's single-pool-per-topology norm.
|
|
dom_mo.add_child(MO(
|
|
"infraRsVlanNs",
|
|
dn=f"{dom_dn}/rsvlanNs",
|
|
tDn=first_pool_dn,
|
|
))
|
|
return dom_mo
|
|
|
|
|
|
def _build_aaep(
|
|
aaep: AAEP,
|
|
phys_domains: list[PhysDomain],
|
|
l3_domains: list[L3Domain],
|
|
) -> MO:
|
|
"""Build infraAttEntityP + infraRsDomP children for all domains."""
|
|
aaep_dn = f"uni/infra/attentp-{aaep.name}"
|
|
aaep_mo = MO("infraAttEntityP", dn=aaep_dn, name=aaep.name)
|
|
for phys_dom in phys_domains:
|
|
tdn = f"uni/phys-{phys_dom.name}"
|
|
aaep_mo.add_child(MO(
|
|
"infraRsDomP",
|
|
dn=f"{aaep_dn}/rsdomP-[{tdn}]",
|
|
tDn=tdn,
|
|
))
|
|
for l3_dom in l3_domains:
|
|
tdn = f"uni/l3dom-{l3_dom.name}"
|
|
aaep_mo.add_child(MO(
|
|
"infraRsDomP",
|
|
dn=f"{aaep_dn}/rsdomP-[{tdn}]",
|
|
tDn=tdn,
|
|
))
|
|
return aaep_mo
|
|
|
|
|
|
def _build_port_ipg(aaep: AAEP) -> MO:
|
|
"""Build infraAccPortGrp + infraRsAttEntP child for one AAEP."""
|
|
ipg_name = f"{aaep.name}-ipg"
|
|
ipg_dn = f"uni/infra/funcprof/accportgrp-{ipg_name}"
|
|
ipg_mo = MO(
|
|
"infraAccPortGrp",
|
|
dn=ipg_dn,
|
|
name=ipg_name,
|
|
lagT="not-aggregated",
|
|
)
|
|
ipg_mo.add_child(MO(
|
|
"infraRsAttEntP",
|
|
dn=f"{ipg_dn}/rsattEntP",
|
|
tDn=f"uni/infra/attentp-{aaep.name}",
|
|
))
|
|
return ipg_mo
|
|
|
|
|
|
def _build_bndl_grp(name: str, lag_t: str) -> MO:
|
|
"""Build infraAccBndlGrp (funcprof bundle IPG) — vPC (lagT=node) or PC (lagT=link)."""
|
|
dn = f"uni/infra/funcprof/accbundle-{name}"
|
|
return MO("infraAccBndlGrp", dn=dn, name=name, lagT=lag_t)
|
|
|
|
|
|
def _build_hports(
|
|
accportp_dn: str,
|
|
sel_name: str,
|
|
from_port: int,
|
|
to_port: int,
|
|
bndl_grp_dn: str,
|
|
) -> MO:
|
|
"""Build infraHPortS (type=range) + infraPortBlk + infraRsAccBaseGrp children.
|
|
|
|
RN scheme verified against this module's own uni/infra/... conventions
|
|
(accportprof-/hports-/portblk-):
|
|
infraHPortS hports-{sel_name}-typ-range
|
|
infraPortBlk portblk-block1 (single block covering the range)
|
|
infraRsAccBaseGrp rsaccBaseGrp (fixed RN — one target per selector)
|
|
"""
|
|
hports_dn = f"{accportp_dn}/hports-{sel_name}-typ-range"
|
|
hports_mo = MO("infraHPortS", dn=hports_dn, name=sel_name, type="range")
|
|
hports_mo.add_child(MO(
|
|
"infraPortBlk",
|
|
dn=f"{hports_dn}/portblk-block1",
|
|
fromPort=str(from_port),
|
|
toPort=str(to_port),
|
|
fromCard="1",
|
|
))
|
|
hports_mo.add_child(MO(
|
|
"infraRsAccBaseGrp",
|
|
dn=f"{hports_dn}/rsaccBaseGrp",
|
|
tDn=bndl_grp_dn,
|
|
))
|
|
return hports_mo
|
|
|
|
|
|
def _build_leaf_profile(profile_name: str, sel_name: str, bndl_grp_dn: str) -> MO:
|
|
"""Build one infraAccPortP with one infraHPortS selector spanning the
|
|
leaf's host-port range (eth1/1.._HOST_PORTS — the exact inventory
|
|
interfaces.py builds), so every infraPortBlk from/to port exists as a
|
|
real l1PhysIf."""
|
|
accportp_dn = f"uni/infra/accportprof-{profile_name}"
|
|
accportp_mo = MO("infraAccPortP", dn=accportp_dn, name=profile_name)
|
|
accportp_mo.add_child(_build_hports(accportp_dn, sel_name, 1, _HOST_PORTS, bndl_grp_dn))
|
|
return accportp_mo
|
|
|
|
|
|
def build(topo: Topology, site: Site, store: MITStore) -> None:
|
|
"""Emit access-policy MOs (site-independent, build once)."""
|
|
# Batch-1 fix: uni/infra previously had no MO of its own — only
|
|
# descendant DNs (uni/infra/vlanns-.../, uni/infra/attentp-.../, etc.),
|
|
# which the store's ancestor-walk (_register) links structurally but
|
|
# never materializes as a real object. That made
|
|
# GET /api/mo/uni/infra.json (any rsp-subtree mode) 404, even though the
|
|
# generic query engine's rsp-subtree=full traversal otherwise works fine
|
|
# (verified: /api/class/infraAccPortP.json?rsp-subtree=full already
|
|
# returns the nested infraHPortS/infraPortBlk/infraRsAccBaseGrp tree).
|
|
# infraInfra is the real ACI class for this container (CONTRACT.md §6
|
|
# "Infra write targets"); seeding it here is a one-line, no-behavior-
|
|
# change fix so `GET /api/mo/uni/infra.json?rsp-subtree=full` (needed to
|
|
# see the whole access-policy tree in one call, same as autoACI's
|
|
# ipg_detail/access_policy plugins would) returns 200 with a real root.
|
|
store.add(MO("infraInfra", dn="uni/infra", name=""))
|
|
|
|
first_pool_dn = (
|
|
f"uni/infra/vlanns-[{topo.access.vlan_pools[0].name}]-static"
|
|
if topo.access.vlan_pools else ""
|
|
)
|
|
|
|
for pool in topo.access.vlan_pools:
|
|
store.add(_build_vlan_pool(pool))
|
|
|
|
for phys_dom in topo.access.phys_domains:
|
|
store.add(_build_phys_dom(phys_dom, first_pool_dn))
|
|
|
|
for l3_dom in topo.access.l3_domains:
|
|
store.add(_build_l3_dom(l3_dom, first_pool_dn))
|
|
|
|
# Tier-2 (PR-19): VMM domains — optional, defaults to an empty list, so
|
|
# a topology with no `access.vmm_domains` produces zero vmmDomP MOs
|
|
# (byte-identical to pre-PR-19 output).
|
|
for vmm in topo.access.vmm_domains:
|
|
store.add(_build_vmm_domain(vmm, first_pool_dn))
|
|
|
|
for aaep in topo.access.aaeps:
|
|
store.add(_build_aaep(aaep, topo.access.phys_domains, topo.access.l3_domains))
|
|
|
|
for aaep in topo.access.aaeps:
|
|
store.add(_build_port_ipg(aaep))
|
|
|
|
# ---- Batch-1: leaf-interface-policy cluster --------------------------
|
|
# One vPC bundle group per border-leaf vPC domain (fabric.py already
|
|
# builds the matching vpcDom/vpcIf for these same pairs), one leaf
|
|
# interface-profile per leaf pair (regular leaves + each border-leaf
|
|
# vPC pair), each with a single range selector spanning the real
|
|
# host-port inventory (eth1/1.._HOST_PORTS).
|
|
seen_vpc_domains: set[str] = set()
|
|
for bl in site.border_leaves:
|
|
if bl.vpc_domain in seen_vpc_domains:
|
|
continue
|
|
seen_vpc_domains.add(bl.vpc_domain)
|
|
bndl_name = f"{bl.vpc_domain}-vpc"
|
|
store.add(_build_bndl_grp(bndl_name, lag_t="node"))
|
|
bndl_grp_dn = f"uni/infra/funcprof/accbundle-{bndl_name}"
|
|
profile_name = f"{site.name}-{bl.vpc_domain}"
|
|
store.add(_build_leaf_profile(profile_name, f"sel-{bl.vpc_domain}", bndl_grp_dn))
|
|
|
|
leaves = site.leaf_nodes()
|
|
if len(leaves) >= 2:
|
|
pair_name = f"{site.name}-leafpair"
|
|
pc_bndl_name = f"{pair_name}-pc"
|
|
store.add(_build_bndl_grp(pc_bndl_name, lag_t="link"))
|
|
pc_bndl_grp_dn = f"uni/infra/funcprof/accbundle-{pc_bndl_name}"
|
|
store.add(_build_leaf_profile(pair_name, f"sel-{pair_name}", pc_bndl_grp_dn))
|