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>
173 lines
6.6 KiB
Python
173 lines
6.6 KiB
Python
"""Regression tests for P1 review findings F1 (tree-add children leak/dup) and
|
|
F2 (rsp-subtree=children must be one level, full must be recursive).
|
|
|
|
Builders add MOs as TREES (MO.add_child), the exact pattern the original
|
|
flat-only tests never exercised.
|
|
"""
|
|
|
|
from aci_sim.mit.mo import MO
|
|
from aci_sim.mit.store import MITStore
|
|
from aci_sim.query.engine import QueryParams, run_class_query, run_mo_query
|
|
|
|
|
|
def _tenant_tree() -> MO:
|
|
"""uni/tn-T ▷ BD-b ▷ subnet-[10.0.0.1/24] (a 3-level tree)."""
|
|
tn = MO("fvTenant", dn="uni/tn-T", name="T")
|
|
bd = MO("fvBD", dn="uni/tn-T/BD-b", name="b")
|
|
sn = MO("fvSubnet", dn="uni/tn-T/BD-b/subnet-[10.0.0.1/24]", ip="10.0.0.1/24")
|
|
bd.add_child(sn)
|
|
tn.add_child(bd)
|
|
return tn
|
|
|
|
|
|
# --- F1: adding a tree must not leak/duplicate children ---------------------
|
|
|
|
|
|
def test_tree_add_no_children_leak_on_nonsubtree_class_query():
|
|
s = MITStore()
|
|
s.add(_tenant_tree())
|
|
imdata, total = run_class_query(s, "fvBD", QueryParams()) # no subtree requested
|
|
assert total == 1
|
|
assert "children" not in imdata[0]["fvBD"], "children must be absent without a subtree mode (CONTRACT §2)"
|
|
|
|
|
|
def test_tree_add_no_children_leak_on_nonsubtree_mo_query():
|
|
s = MITStore()
|
|
s.add(_tenant_tree())
|
|
imdata, _ = run_mo_query(s, "uni/tn-T", QueryParams())
|
|
assert "children" not in imdata[0]["fvTenant"]
|
|
|
|
|
|
def test_tree_add_subtree_query_lists_each_descendant_once():
|
|
s = MITStore()
|
|
s.add(_tenant_tree())
|
|
imdata, _ = run_mo_query(s, "uni/tn-T", QueryParams(query_target="subtree"))
|
|
# Flatten every dn that appears anywhere in the imdata payload.
|
|
seen: list[str] = []
|
|
|
|
def walk(entry: dict):
|
|
for _cls, body in entry.items():
|
|
dn = body["attributes"].get("dn")
|
|
if dn:
|
|
seen.append(dn)
|
|
for child in body.get("children", []):
|
|
walk(child)
|
|
|
|
for entry in imdata:
|
|
walk(entry)
|
|
# root + bd + subnet, each exactly once
|
|
assert seen.count("uni/tn-T") == 1
|
|
assert seen.count("uni/tn-T/BD-b") == 1
|
|
assert seen.count("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == 1
|
|
|
|
|
|
# --- F2: children == one level, full == recursive ---------------------------
|
|
|
|
|
|
def _child_classes(imdata_entry: dict) -> set[str]:
|
|
body = next(iter(imdata_entry.values()))
|
|
return {next(iter(c)) for c in body.get("children", [])}
|
|
|
|
|
|
def test_rsp_subtree_children_is_one_level():
|
|
s = MITStore()
|
|
s.add(_tenant_tree())
|
|
imdata, _ = run_class_query(s, "fvTenant", QueryParams(rsp_subtree="children"))
|
|
classes = _child_classes(imdata[0])
|
|
assert "fvBD" in classes
|
|
assert "fvSubnet" not in classes, "children mode is direct children only"
|
|
|
|
|
|
def test_rsp_subtree_full_is_recursive():
|
|
s = MITStore()
|
|
s.add(_tenant_tree())
|
|
imdata, _ = run_class_query(s, "fvTenant", QueryParams(rsp_subtree="full"))
|
|
tn = imdata[0]["fvTenant"]
|
|
# full mode returns the WHOLE subtree, and it stays NESTED — the grandchild
|
|
# (fvSubnet) hangs under its parent (fvBD), NOT flattened onto the tenant.
|
|
# (B1 regression: flattening silently emptied autoACI's two-level readers
|
|
# like contract_map vzBrCP→vzSubj→vzRsSubjFiltAtt.)
|
|
assert _child_classes(imdata[0]) == {"fvBD"}, "grandchildren must not be hoisted to the root"
|
|
bd = tn["children"][0]["fvBD"]
|
|
assert {next(iter(c)) for c in bd["children"]} == {"fvSubnet"}, "full mode is recursive"
|
|
|
|
|
|
# --- Legacy query-target=subtree must be FLAT top-level (P3 review Finding 1) ----
|
|
|
|
|
|
def _bgp_inst_tree() -> MO:
|
|
"""spine sys/bgp/inst ▷ dom-overlay-1 ▷ peer (ISN /32) — the real ISN shape."""
|
|
inst = MO("bgpInst", dn="topology/pod-1/node-201/sys/bgp/inst", asn="65001")
|
|
dom = MO("bgpDom", dn="topology/pod-1/node-201/sys/bgp/inst/dom-overlay-1", name="overlay-1")
|
|
peer = MO(
|
|
"bgpPeer",
|
|
dn="topology/pod-1/node-201/sys/bgp/inst/dom-overlay-1/peer-[10.1.401.1]",
|
|
addr="10.1.401.1/32",
|
|
asn="65002",
|
|
)
|
|
dom.add_child(peer)
|
|
inst.add_child(dom)
|
|
return inst
|
|
|
|
|
|
def test_legacy_subtree_returns_target_class_flat_at_top_level():
|
|
"""autoACI's query_dn(subtree=True, subtree_class="bgpPeer") reads item.get('bgpPeer')
|
|
at the TOP level of imdata — the ISN detection depends on this."""
|
|
s = MITStore()
|
|
s.add(_bgp_inst_tree())
|
|
imdata, total = run_mo_query(
|
|
s,
|
|
"topology/pod-1/node-201/sys/bgp/inst",
|
|
QueryParams(query_target="subtree", target_subtree_class=["bgpPeer"]),
|
|
)
|
|
assert total == 1
|
|
assert list(imdata[0].keys()) == ["bgpPeer"], "legacy subtree = class flat at top level, not nested"
|
|
assert imdata[0]["bgpPeer"]["attributes"]["addr"] == "10.1.401.1/32"
|
|
assert "children" not in imdata[0]["bgpPeer"]
|
|
assert all("bgpInst" not in e for e in imdata), "root excluded when it doesn't match the class filter"
|
|
|
|
|
|
def test_legacy_subtree_no_class_filter_is_flat_root_plus_descendants():
|
|
s = MITStore()
|
|
s.add(_bgp_inst_tree())
|
|
imdata, total = run_mo_query(
|
|
s,
|
|
"topology/pod-1/node-201/sys/bgp/inst",
|
|
QueryParams(query_target="subtree"),
|
|
)
|
|
classes = {next(iter(e)) for e in imdata}
|
|
assert classes == {"bgpInst", "bgpDom", "bgpPeer"}
|
|
assert total == 3
|
|
for e in imdata:
|
|
assert "children" not in next(iter(e.values())), "legacy subtree is flat, never nested"
|
|
|
|
|
|
# --- Store hardening: subtree traverses MO-less structural intermediates ------
|
|
# (ep_tracker queries epmMacEp via a node/sys subtree; the epm containers
|
|
# ctx-[]/bd-[]/db-ep have no MO of their own — reachability must still hold.)
|
|
|
|
|
|
def test_subtree_reaches_deep_mo_without_intermediate_mos():
|
|
s = MITStore()
|
|
node_sys = "topology/pod-1/node-101/sys"
|
|
s.add(MO("topSystem", dn=node_sys, role="leaf"))
|
|
deep = f"{node_sys}/ctx-[vxlan-2900001]/bd-[vxlan-15000001]/db-ep/mac-00:50:56:00:00:01"
|
|
s.add(MO("epmMacEp", dn=deep, addr="00:50:56:00:00:01"))
|
|
found = s.subtree(node_sys, classes={"epmMacEp"})
|
|
assert [m.dn for m in found] == [deep], "deep MO reachable via subtree despite MO-less containers"
|
|
# the MO-less containers are NOT surfaced as direct children
|
|
assert s.children(node_sys) == []
|
|
|
|
|
|
def test_legacy_subtree_reaches_deep_epm_via_engine():
|
|
s = MITStore()
|
|
node_sys = "topology/pod-1/node-101/sys"
|
|
s.add(MO("topSystem", dn=node_sys, role="leaf"))
|
|
deep = f"{node_sys}/ctx-[vxlan-2900001]/bd-[vxlan-15000001]/db-ep/mac-00:50:56:00:00:01"
|
|
s.add(MO("epmMacEp", dn=deep, addr="00:50:56:00:00:01"))
|
|
imdata, total = run_mo_query(
|
|
s, node_sys, QueryParams(query_target="subtree", target_subtree_class=["epmMacEp"])
|
|
)
|
|
assert total == 1
|
|
assert imdata[0]["epmMacEp"]["attributes"]["addr"] == "00:50:56:00:00:01"
|