Files
aci-sim/tests/test_pr8_platform.py
T
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

207 lines
6.5 KiB
Python

"""Tests for PR-8 cross-platform + deployment engineering (findings #23-#28).
Covers:
- SIM_BIND default/override parsing (runtime/config.py, #23)
- port-selection function (A/B/fallback) (runtime/supervisor.py, #25)
- CSW peer/border-leaf mismatch validation raises (topology/schema.py, #28)
- `bash -n` syntax check for both sandbox scripts (#24)
"""
from __future__ import annotations
import importlib
import subprocess
import sys
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from aci_sim.topology.schema import Topology
REPO_ROOT = Path(__file__).parent.parent
# ---------------------------------------------------------------------------
# SIM_BIND (#23)
# ---------------------------------------------------------------------------
class TestSimBind:
def test_default_is_loopback(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("SIM_BIND", raising=False)
from aci_sim.runtime import config
importlib.reload(config)
assert config.SIM_BIND == "127.0.0.1"
def test_override_via_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SIM_BIND", "0.0.0.0")
from aci_sim.runtime import config
importlib.reload(config)
try:
assert config.SIM_BIND == "0.0.0.0"
finally:
monkeypatch.delenv("SIM_BIND", raising=False)
importlib.reload(config) # restore default for any later importers
# ---------------------------------------------------------------------------
# Port selection (#25)
# ---------------------------------------------------------------------------
class TestApicPortForSite:
def test_site_zero_uses_apic_a_port(self) -> None:
from aci_sim.runtime.config import APIC_A_PORT
from aci_sim.runtime.supervisor import apic_port_for_site
assert apic_port_for_site(0) == APIC_A_PORT
def test_site_one_uses_apic_b_port(self) -> None:
"""APIC_B_PORT (config.py) must actually be honored for the second
site, not silently shadowed by an APIC_A_PORT+i formula."""
from aci_sim.runtime.config import APIC_B_PORT
from aci_sim.runtime.supervisor import apic_port_for_site
assert apic_port_for_site(1) == APIC_B_PORT
def test_site_two_falls_back_to_apic_a_port_plus_index(self) -> None:
from aci_sim.runtime.config import APIC_A_PORT
from aci_sim.runtime.supervisor import apic_port_for_site
assert apic_port_for_site(2) == APIC_A_PORT + 2
def test_apic_b_port_env_override_takes_effect(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("APIC_B_PORT", "9999")
from aci_sim.runtime import config
importlib.reload(config)
from aci_sim.runtime import supervisor
importlib.reload(supervisor)
try:
assert supervisor.apic_port_for_site(1) == 9999
finally:
monkeypatch.delenv("APIC_B_PORT", raising=False)
importlib.reload(config)
importlib.reload(supervisor)
# ---------------------------------------------------------------------------
# CSW mismatch validation (#28)
# ---------------------------------------------------------------------------
_BASE_TOPOLOGY = {
"fabric": {"name": "test-fabric"},
"sites": [
{
"name": "SiteA",
"id": "1",
"apic_host": "127.0.0.1:8443",
"asn": 65001,
"pod": 1,
"spines": 1,
"leaves": 1,
"cabling": "auto",
"border_leaves": [
{
"id": 103,
"name": "SiteA-BL103",
"vpc_domain": "vpcdom-A",
"csw": {
"name": "CSW01",
"asn": 65100,
"peer_ip": "10.100.0.1",
"local_ip": "10.100.0.2",
"vlan": 100,
},
}
],
}
],
"isn": {"enabled": False},
"tenants": [
{
"name": "TN1",
"stretch": False,
"sites": ["SiteA"],
"vrfs": [{"name": "vrf1"}],
"bds": [],
"aps": [],
"contracts": [],
"l3outs": [
{
"name": "l3o-test",
"vrf": "vrf1",
"site": "SiteA",
"border_leaves": [103],
"csw_peer": {
"name": "CSW01",
"asn": 65100,
"peer_ip": "10.100.0.1",
},
}
],
}
],
}
def _deep_copy(d):
return yaml.safe_load(yaml.safe_dump(d))
class TestCswMismatchValidation:
def test_matching_csw_peer_loads_fine(self) -> None:
Topology.model_validate(_deep_copy(_BASE_TOPOLOGY))
def test_mismatched_asn_raises(self) -> None:
data = _deep_copy(_BASE_TOPOLOGY)
data["tenants"][0]["l3outs"][0]["csw_peer"]["asn"] = 65999
with pytest.raises(ValidationError, match="does not match border-leaf"):
Topology.model_validate(data)
def test_mismatched_peer_ip_raises(self) -> None:
data = _deep_copy(_BASE_TOPOLOGY)
data["tenants"][0]["l3outs"][0]["csw_peer"]["peer_ip"] = "10.100.0.99"
with pytest.raises(ValidationError, match="does not match border-leaf"):
Topology.model_validate(data)
def test_mismatched_name_raises(self) -> None:
data = _deep_copy(_BASE_TOPOLOGY)
data["tenants"][0]["l3outs"][0]["csw_peer"]["name"] = "CSW-WRONG"
with pytest.raises(ValidationError, match="does not match border-leaf"):
Topology.model_validate(data)
# ---------------------------------------------------------------------------
# Sandbox scripts syntax (#24)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"script",
["scripts/sandbox-up.sh", "scripts/sandbox-down.sh"],
)
def test_sandbox_script_bash_syntax_ok(script: str) -> None:
result = subprocess.run(
["bash", "-n", str(REPO_ROOT / script)],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
@pytest.mark.parametrize(
"script",
["scripts/sandbox-up.sh", "scripts/sandbox-down.sh"],
)
def test_sandbox_script_has_linux_branch(script: str) -> None:
text = (REPO_ROOT / script).read_text()
assert "Linux" in text
assert "ip addr" in text