Files
taodengandClaude Fable 5 7e9a175ce6 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>
2026-07-05 20:06:36 +10:00

331 lines
10 KiB
Python

"""Tests for mit/ — dn, mo, store."""
import copy
import pytest
from aci_sim.mit.dn import (
dn_split, parent_dn, rn_of, name_from_dn, pod_from_dn, dn_class_hint,
)
from aci_sim.mit.mo import MO
from aci_sim.mit.store import MITStore
# ---------------------------------------------------------------------------
# dn.py
# ---------------------------------------------------------------------------
class TestDnSplit:
def test_simple_three_parts(self):
assert dn_split("uni/tn-T/BD-b") == ["uni", "tn-T", "BD-b"]
def test_subnet_bracket(self):
# CONTRACT example: slash inside brackets must NOT split
assert dn_split("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == [
"uni", "tn-T", "BD-b", "subnet-[10.0.0.1/24]"
]
def test_pathep_bracket(self):
# CONTRACT example
assert dn_split("uni/tn-T/pathep-[eth1/9]") == [
"uni", "tn-T", "pathep-[eth1/9]"
]
def test_rsdomatt_bracket(self):
# CONTRACT example: bracket value itself contains a slash
assert dn_split("uni/phys-X/rsdomAtt-[uni/phys-X]") == [
"uni", "phys-X", "rsdomAtt-[uni/phys-X]"
]
def test_nested_brackets(self):
# e.g. rslldpIfAtt-[pathep-[eth1/9]] — nested brackets, one slash inside
parts = dn_split("uni/infra/rslldpIfAtt-[pathep-[eth1/9]]")
assert parts == ["uni", "infra", "rslldpIfAtt-[pathep-[eth1/9]]"]
def test_single_component(self):
assert dn_split("uni") == ["uni"]
def test_empty(self):
assert dn_split("") == []
def test_no_brackets(self):
assert dn_split("a/b/c/d") == ["a", "b", "c", "d"]
class TestParentDn:
def test_three_deep(self):
assert parent_dn("uni/tn-T/BD-b") == "uni/tn-T"
def test_top_level(self):
assert parent_dn("uni") == ""
def test_bracket_rn(self):
# parent of a bracket RN is the surrounding path
assert parent_dn("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == "uni/tn-T/BD-b"
class TestRnOf:
def test_basic(self):
assert rn_of("uni/tn-T/BD-b") == "BD-b"
def test_bracket(self):
assert rn_of("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == "subnet-[10.0.0.1/24]"
def test_single(self):
assert rn_of("uni") == "uni"
class TestNameFromDn:
def test_plain(self):
assert name_from_dn("uni/tn-T") == "T"
def test_bd(self):
assert name_from_dn("uni/tn-T/BD-mybd") == "mybd"
def test_subnet_bracket(self):
assert name_from_dn("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == "10.0.0.1/24"
def test_pathep_bracket(self):
# pathep-[eth1/9] → eth1/9
assert name_from_dn("topology/pod-1/node-101/local/svc-x/pathep-[eth1/9]") == "eth1/9"
def test_rsdomatt_bracket(self):
assert name_from_dn("uni/phys-X/rsdomAtt-[uni/phys-X]") == "uni/phys-X"
def test_no_dash(self):
assert name_from_dn("uni") == "uni"
class TestPodFromDn:
def test_pod_1(self):
assert pod_from_dn("topology/pod-1/node-101") == "1"
def test_pod_2(self):
assert pod_from_dn("topology/pod-2/node-201/sys") == "2"
def test_no_pod_default_1(self):
assert pod_from_dn("uni/tn-T") == "1"
def test_pod_at_end(self):
assert pod_from_dn("topology/pod-3") == "3"
def test_pod_prefix_dn(self):
assert pod_from_dn("topology/pod-10/node-1001/sys/health") == "10"
class TestDnClassHint:
def test_tn(self):
assert dn_class_hint("tn-T") == "fvTenant"
def test_uni(self):
assert dn_class_hint("uni") == "polUni"
def test_unknown(self):
assert dn_class_hint("foo-bar") is None
# ---------------------------------------------------------------------------
# mo.py
# ---------------------------------------------------------------------------
class TestMO:
def test_construction(self):
mo = MO("fvTenant", dn="uni/tn-T", name="T")
assert mo.class_name == "fvTenant"
assert mo.dn == "uni/tn-T"
assert mo.attrs["name"] == "T"
def test_add_child(self):
parent = MO("fvTenant", dn="uni/tn-T")
child = MO("fvCtx", dn="uni/tn-T/ctx-v1", name="v1")
parent.add_child(child)
assert parent.children == [child]
def test_flat_depth_first(self):
root = MO("fvTenant", dn="uni/tn-T")
c1 = MO("fvCtx", dn="uni/tn-T/ctx-v1")
c2 = MO("fvBD", dn="uni/tn-T/BD-b")
c2a = MO("fvSubnet", dn="uni/tn-T/BD-b/subnet-[10.0.0.1/24]")
root.add_child(c1)
root.add_child(c2)
c2.add_child(c2a)
assert root.flat() == [root, c1, c2, c2a]
def test_to_imdata_no_children(self):
mo = MO("fvTenant", dn="uni/tn-T", name="T")
d = mo.to_imdata()
assert list(d.keys()) == ["fvTenant"]
assert "children" not in d["fvTenant"]
assert d["fvTenant"]["attributes"]["dn"] == "uni/tn-T"
def test_to_imdata_with_children(self):
parent = MO("fvTenant", dn="uni/tn-T")
child = MO("fvCtx", dn="uni/tn-T/ctx-v1", name="v1")
parent.add_child(child)
d = parent.to_imdata()
assert "children" in d["fvTenant"]
child_d = d["fvTenant"]["children"][0]
assert "fvCtx" in child_d
assert child_d["fvCtx"]["attributes"]["name"] == "v1"
def test_to_imdata_attrs_are_copy(self):
mo = MO("fvTenant", dn="uni/tn-T", name="T")
d = mo.to_imdata()
d["fvTenant"]["attributes"]["name"] = "mutated"
assert mo.attrs["name"] == "T" # original unchanged
# ---------------------------------------------------------------------------
# store.py
# ---------------------------------------------------------------------------
def _flat_store() -> MITStore:
"""Return a store populated with a small tree (all MOs added individually)."""
s = MITStore()
s.add(MO("fvTenant", dn="uni/tn-T", name="T"))
s.add(MO("fvCtx", dn="uni/tn-T/ctx-v1", name="v1"))
s.add(MO("fvBD", dn="uni/tn-T/BD-b", name="b"))
s.add(MO("fvSubnet", dn="uni/tn-T/BD-b/subnet-[10.0.0.1/24]", ip="10.0.0.1/24"))
return s
class TestMITStoreGet:
def test_get_existing(self):
s = _flat_store()
mo = s.get("uni/tn-T/ctx-v1")
assert mo is not None
assert mo.class_name == "fvCtx"
def test_get_missing(self):
assert _flat_store().get("uni/tn-X") is None
def test_get_bracket_dn(self):
s = _flat_store()
mo = s.get("uni/tn-T/BD-b/subnet-[10.0.0.1/24]")
assert mo is not None
assert mo.attrs["ip"] == "10.0.0.1/24"
class TestMITStoreChildren:
def test_children_of_tenant(self):
s = _flat_store()
kids = s.children("uni/tn-T")
dns = {mo.dn for mo in kids}
assert "uni/tn-T/ctx-v1" in dns
assert "uni/tn-T/BD-b" in dns
def test_children_class_filter(self):
s = _flat_store()
ctxs = s.children("uni/tn-T", classes={"fvCtx"})
assert len(ctxs) == 1
assert ctxs[0].class_name == "fvCtx"
def test_children_leaf_node_empty(self):
s = _flat_store()
assert s.children("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == []
class TestMITStoreSubtree:
def test_all_descendants(self):
s = _flat_store()
desc = s.subtree("uni/tn-T")
dns = {mo.dn for mo in desc}
assert "uni/tn-T/ctx-v1" in dns
assert "uni/tn-T/BD-b" in dns
assert "uni/tn-T/BD-b/subnet-[10.0.0.1/24]" in dns
# Root itself must NOT be included
assert "uni/tn-T" not in dns
def test_class_filter(self):
s = _flat_store()
subs = s.subtree("uni/tn-T", classes={"fvSubnet"})
assert len(subs) == 1
assert subs[0].class_name == "fvSubnet"
def test_class_filter_traverses_full_tree(self):
"""A class filter must still find deeply-nested matches."""
s = _flat_store()
# fvSubnet is 3 levels below uni/tn-T; asking for it from the top works
subs = s.subtree("uni/tn-T", classes={"fvSubnet"})
assert len(subs) == 1
def test_empty_subtree(self):
s = _flat_store()
assert s.subtree("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") == []
class TestMITStoreByClass:
def test_by_class_present(self):
s = _flat_store()
bds = s.by_class("fvBD")
assert len(bds) == 1
assert bds[0].dn == "uni/tn-T/BD-b"
def test_by_class_absent(self):
assert _flat_store().by_class("fvAp") == []
class TestMITStoreAll:
def test_insertion_order(self):
s = _flat_store()
dns = [mo.dn for mo in s.all()]
assert dns == [
"uni/tn-T",
"uni/tn-T/ctx-v1",
"uni/tn-T/BD-b",
"uni/tn-T/BD-b/subnet-[10.0.0.1/24]",
]
class TestMITStoreUpsert:
def test_upsert_merges_attrs(self):
s = _flat_store()
s.upsert(MO("fvTenant", dn="uni/tn-T", name="T", descr="new-desc"))
mo = s.get("uni/tn-T")
assert mo.attrs["descr"] == "new-desc"
assert mo.attrs["name"] == "T" # original attr preserved
def test_upsert_inserts_new(self):
s = _flat_store()
s.upsert(MO("fvTenant", dn="uni/tn-T2", name="T2"))
assert s.get("uni/tn-T2") is not None
def test_upsert_recurses_children(self):
s = MITStore()
root = MO("fvTenant", dn="uni/tn-T")
child = MO("fvCtx", dn="uni/tn-T/ctx-v1", name="v1")
root.add_child(child)
s.upsert(root)
assert s.get("uni/tn-T") is not None
assert s.get("uni/tn-T/ctx-v1") is not None
def test_upsert_deleted_removes_dn(self):
s = _flat_store()
s.upsert(MO("fvBD", dn="uni/tn-T/BD-b", status="deleted"))
assert s.get("uni/tn-T/BD-b") is None
def test_upsert_deleted_removes_subtree(self):
s = _flat_store()
s.upsert(MO("fvBD", dn="uni/tn-T/BD-b", status="deleted"))
assert s.get("uni/tn-T/BD-b/subnet-[10.0.0.1/24]") is None
def test_upsert_deleted_keeps_siblings(self):
s = _flat_store()
s.upsert(MO("fvBD", dn="uni/tn-T/BD-b", status="deleted"))
assert s.get("uni/tn-T") is not None
assert s.get("uni/tn-T/ctx-v1") is not None
class TestMITStoreDeepCopy:
def test_deepcopy_isolates(self):
s = _flat_store()
s2 = copy.deepcopy(s)
s.upsert(MO("fvTenant", dn="uni/tn-T", name="mutated"))
assert s2.get("uni/tn-T").attrs["name"] == "T"
def test_deepcopy_is_complete(self):
s = _flat_store()
s2 = copy.deepcopy(s)
assert {mo.dn for mo in s2.all()} == {mo.dn for mo in s.all()}