mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-21 21:15:12 +00:00
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:
@@ -0,0 +1,620 @@
|
||||
"""Tests for topology/schema.py and topology/loader.py (Phase 2).
|
||||
|
||||
All tests load the default topology.yaml at the repo root. One negative test
|
||||
builds a minimal bad topology inline to verify cross-reference validation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from aci_sim.topology.loader import load_topology
|
||||
from aci_sim.topology.schema import Node, Topology
|
||||
|
||||
TOPOLOGY_YAML = Path(__file__).parent.parent / "topology.yaml"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_topo() -> Topology:
|
||||
return load_topology(TOPOLOGY_YAML)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Positive tests against default topology.yaml
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDefaultTopologyLoad:
|
||||
def test_has_two_sites(self) -> None:
|
||||
topo = get_topo()
|
||||
assert len(topo.sites) == 2
|
||||
|
||||
def test_site_names(self) -> None:
|
||||
topo = get_topo()
|
||||
names = [s.name for s in topo.sites]
|
||||
assert "LAB1" in names
|
||||
assert "LAB2" in names
|
||||
|
||||
def test_site_a_spine_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_a = topo.site_by_name("LAB1")
|
||||
assert len(site_a.spine_nodes()) == 2
|
||||
|
||||
def test_site_a_leaf_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_a = topo.site_by_name("LAB1")
|
||||
assert len(site_a.leaf_nodes()) == 2
|
||||
|
||||
def test_site_a_border_leaf_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_a = topo.site_by_name("LAB1")
|
||||
assert len(site_a.border_leaves) == 2
|
||||
|
||||
def test_site_b_spine_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_b = topo.site_by_name("LAB2")
|
||||
assert len(site_b.spine_nodes()) == 2
|
||||
|
||||
def test_site_b_leaf_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_b = topo.site_by_name("LAB2")
|
||||
assert len(site_b.leaf_nodes()) == 2
|
||||
|
||||
def test_site_b_border_leaf_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_b = topo.site_by_name("LAB2")
|
||||
assert len(site_b.border_leaves) == 2
|
||||
|
||||
|
||||
class TestNodeIdExpansion:
|
||||
"""Int-count → Node expansion produces deterministic, collision-free IDs."""
|
||||
|
||||
def test_site_a_spine_ids(self) -> None:
|
||||
# site_index=0: spine_id = 201 + 0*200 + i → 201, 202
|
||||
topo = get_topo()
|
||||
ids = sorted(n.id for n in topo.site_by_name("LAB1").spine_nodes())
|
||||
assert ids == [201, 202]
|
||||
|
||||
def test_site_a_leaf_ids(self) -> None:
|
||||
# site_index=0: leaf_id = 101 + 0*200 + i → 101, 102
|
||||
topo = get_topo()
|
||||
ids = sorted(n.id for n in topo.site_by_name("LAB1").leaf_nodes())
|
||||
assert ids == [101, 102]
|
||||
|
||||
def test_site_b_spine_ids(self) -> None:
|
||||
# site_index=1: spine_id = 201 + 1*200 + i → 401, 402
|
||||
topo = get_topo()
|
||||
ids = sorted(n.id for n in topo.site_by_name("LAB2").spine_nodes())
|
||||
assert ids == [401, 402]
|
||||
|
||||
def test_site_b_leaf_ids(self) -> None:
|
||||
# site_index=1: leaf_id = 101 + 1*200 + i → 301, 302
|
||||
topo = get_topo()
|
||||
ids = sorted(n.id for n in topo.site_by_name("LAB2").leaf_nodes())
|
||||
assert ids == [301, 302]
|
||||
|
||||
def test_no_collisions_across_sites(self) -> None:
|
||||
topo = get_topo()
|
||||
all_ids: list[int] = []
|
||||
for site in topo.sites:
|
||||
all_ids.extend(n.id for n in site.all_nodes())
|
||||
|
||||
duplicates = [nid for nid in set(all_ids) if all_ids.count(nid) > 1]
|
||||
assert duplicates == [], f"Duplicate node IDs across sites: {duplicates}"
|
||||
|
||||
def test_spine_names_deterministic(self) -> None:
|
||||
topo = get_topo()
|
||||
site_a = topo.site_by_name("LAB1")
|
||||
names = sorted(n.name for n in site_a.spine_nodes())
|
||||
assert names == ["LAB1-ACI-SP01", "LAB1-ACI-SP02"]
|
||||
|
||||
def test_leaf_names_deterministic(self) -> None:
|
||||
topo = get_topo()
|
||||
site_b = topo.site_by_name("LAB2")
|
||||
names = sorted(n.name for n in site_b.leaf_nodes())
|
||||
assert names == ["LAB2-ACI-LF301", "LAB2-ACI-LF302"]
|
||||
|
||||
|
||||
class TestStretchedTenant:
|
||||
def test_stretched_tenant_exists(self) -> None:
|
||||
topo = get_topo()
|
||||
stretched = [t for t in topo.tenants if t.stretch]
|
||||
assert len(stretched) >= 1, "No stretched tenant found in topology"
|
||||
|
||||
def test_stretched_tenant_in_both_sites(self) -> None:
|
||||
topo = get_topo()
|
||||
for tenant in topo.tenants:
|
||||
if tenant.stretch:
|
||||
assert "LAB1" in tenant.sites, (
|
||||
f"Stretched tenant '{tenant.name}' missing LAB1"
|
||||
)
|
||||
assert "LAB2" in tenant.sites, (
|
||||
f"Stretched tenant '{tenant.name}' missing LAB2"
|
||||
)
|
||||
|
||||
def test_corp_tenant_vrfs_bds_aps(self) -> None:
|
||||
topo = get_topo()
|
||||
corp = next(t for t in topo.tenants if t.name == "MS-TN1")
|
||||
assert len(corp.vrfs) >= 1
|
||||
assert len(corp.bds) >= 1
|
||||
assert len(corp.aps) >= 1
|
||||
|
||||
def test_corp_contracts_non_empty(self) -> None:
|
||||
topo = get_topo()
|
||||
corp = next(t for t in topo.tenants if t.name == "MS-TN1")
|
||||
assert len(corp.contracts) >= 1
|
||||
|
||||
def test_corp_l3outs_per_site(self) -> None:
|
||||
topo = get_topo()
|
||||
corp = next(t for t in topo.tenants if t.name == "MS-TN1")
|
||||
assert len(corp.l3outs) >= 2, "MS-TN1 tenant should have ≥1 L3Out per site"
|
||||
|
||||
|
||||
class TestL3OutVrfResolves:
|
||||
def test_l3out_vrfs_valid(self) -> None:
|
||||
topo = get_topo()
|
||||
for tenant in topo.tenants:
|
||||
vrf_names = {v.name for v in tenant.vrfs}
|
||||
for l3out in tenant.l3outs:
|
||||
assert l3out.vrf in vrf_names, (
|
||||
f"Tenant '{tenant.name}', L3Out '{l3out.name}' references "
|
||||
f"VRF '{l3out.vrf}' not in {vrf_names}"
|
||||
)
|
||||
|
||||
|
||||
class TestHelpers:
|
||||
def test_site_by_name_found(self) -> None:
|
||||
topo = get_topo()
|
||||
site = topo.site_by_name("LAB1")
|
||||
assert site.name == "LAB1"
|
||||
|
||||
def test_site_by_name_missing_raises(self) -> None:
|
||||
topo = get_topo()
|
||||
with pytest.raises(KeyError):
|
||||
topo.site_by_name("Nonexistent")
|
||||
|
||||
def test_other_site_from_a_returns_b(self) -> None:
|
||||
topo = get_topo()
|
||||
other = topo.other_site("LAB1")
|
||||
assert other.name == "LAB2"
|
||||
|
||||
def test_other_site_from_b_returns_a(self) -> None:
|
||||
topo = get_topo()
|
||||
other = topo.other_site("LAB2")
|
||||
assert other.name == "LAB1"
|
||||
|
||||
def test_other_site_gives_remote_asn(self) -> None:
|
||||
"""other_site() should let builders look up the remote fabric ASN for ISN."""
|
||||
topo = get_topo()
|
||||
# LAB1's ISN peer is LAB2's ASN
|
||||
remote_asn = topo.other_site("LAB1").asn
|
||||
assert remote_asn == topo.site_by_name("LAB2").asn
|
||||
|
||||
def test_all_nodes_count(self) -> None:
|
||||
topo = get_topo()
|
||||
site_a = topo.site_by_name("LAB1")
|
||||
# 2 spines + 2 leaves + 2 border leaves = 6
|
||||
assert len(site_a.all_nodes()) == 6
|
||||
|
||||
|
||||
class TestISN:
|
||||
def test_isn_enabled(self) -> None:
|
||||
topo = get_topo()
|
||||
assert topo.isn.enabled is True
|
||||
|
||||
def test_isn_peer_mesh_full(self) -> None:
|
||||
topo = get_topo()
|
||||
assert topo.isn.peer_mesh == "full"
|
||||
|
||||
|
||||
class TestFaultsAndEndpoints:
|
||||
def test_fault_seeds_present(self) -> None:
|
||||
topo = get_topo()
|
||||
assert len(topo.faults.seed) >= 1
|
||||
|
||||
def test_health_defaults(self) -> None:
|
||||
topo = get_topo()
|
||||
assert topo.faults.health_defaults.node > 0
|
||||
assert topo.faults.health_defaults.tenant > 0
|
||||
|
||||
def test_endpoints_per_epg(self) -> None:
|
||||
topo = get_topo()
|
||||
assert topo.endpoints.per_epg >= 1
|
||||
|
||||
|
||||
class TestNewBuilderFields:
|
||||
"""Fields added for the Phase 3/4 builders (version, l3 domains, addr pools)."""
|
||||
|
||||
def test_node_has_version_default(self) -> None:
|
||||
topo = get_topo()
|
||||
for node in topo.site_by_name("LAB1").all_nodes():
|
||||
assert node.version, f"Node {node.id} missing version"
|
||||
|
||||
def test_explicit_node_version_override(self) -> None:
|
||||
n = Node(id=201, name="spine-201", version="n9000-16.0(1)")
|
||||
assert n.version == "n9000-16.0(1)"
|
||||
|
||||
def test_access_has_l3_domain(self) -> None:
|
||||
topo = get_topo()
|
||||
assert len(topo.access.l3_domains) >= 1
|
||||
assert topo.access.l3_domains[0].name
|
||||
|
||||
def test_fabric_address_pools(self) -> None:
|
||||
topo = get_topo()
|
||||
assert topo.fabric.loopback_pool
|
||||
assert topo.fabric.oob_pool
|
||||
|
||||
def test_l3out_site_association(self) -> None:
|
||||
topo = get_topo()
|
||||
corp = next(t for t in topo.tenants if t.name == "MS-TN1")
|
||||
by_name = {lo.name: lo for lo in corp.l3outs}
|
||||
assert by_name["l3o-bgp-Core_LAB1"].site == "LAB1"
|
||||
assert by_name["l3o-bgp-Core_LAB2"].site == "LAB2"
|
||||
|
||||
def test_l3out_border_leaves_are_real(self) -> None:
|
||||
topo = get_topo()
|
||||
all_border_ids = {
|
||||
bl.id for site in topo.sites for bl in site.border_leaves
|
||||
}
|
||||
for tenant in topo.tenants:
|
||||
for l3out in tenant.l3outs:
|
||||
for bl_id in l3out.border_leaves:
|
||||
assert bl_id in all_border_ids
|
||||
|
||||
def test_fault_seed_nodes_are_real(self) -> None:
|
||||
topo = get_topo()
|
||||
real_ids = {n.id for site in topo.sites for n in site.all_nodes()}
|
||||
for fseed in topo.faults.seed:
|
||||
assert fseed.node in real_ids
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Negative test — bad cross-reference raises ValidationError
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestValidationFailures:
|
||||
def _load_bad(self, yaml_text: str) -> None:
|
||||
data = yaml.safe_load(yaml_text)
|
||||
Topology.model_validate(data)
|
||||
|
||||
def test_epg_references_missing_bd_raises(self) -> None:
|
||||
"""EPG.bd that does not exist in tenant.bds must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: BadTenant
|
||||
stretch: false
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds:
|
||||
- name: real-bd
|
||||
vrf: vrf1
|
||||
subnets: []
|
||||
aps:
|
||||
- name: App
|
||||
epgs:
|
||||
- name: bad-epg
|
||||
bd: nonexistent-bd
|
||||
contracts: {}
|
||||
contracts: []
|
||||
l3outs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_bd_references_missing_vrf_raises(self) -> None:
|
||||
"""BD.vrf that does not exist in tenant.vrfs must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: BadTenant
|
||||
stretch: false
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds:
|
||||
- name: bd1
|
||||
vrf: ghost-vrf
|
||||
subnets: []
|
||||
aps: []
|
||||
contracts: []
|
||||
l3outs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_stretched_tenant_with_one_site_raises(self) -> None:
|
||||
"""stretch:true but only one site listed must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
- name: SiteB
|
||||
id: "2"
|
||||
apic_host: "127.0.0.1:8444"
|
||||
asn: 65002
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: true
|
||||
peer_mesh: full
|
||||
tenants:
|
||||
- name: BadStretch
|
||||
stretch: true
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds: []
|
||||
aps: []
|
||||
contracts: []
|
||||
l3outs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_tenant_references_unknown_site_raises(self) -> None:
|
||||
"""tenant.sites that names a non-existent site must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: BadTenant
|
||||
stretch: false
|
||||
sites: [SiteX]
|
||||
vrfs: []
|
||||
bds: []
|
||||
aps: []
|
||||
contracts: []
|
||||
l3outs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_l3out_references_missing_vrf_raises(self) -> None:
|
||||
"""L3Out.vrf that does not exist in tenant.vrfs must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
border_leaves:
|
||||
- id: 103
|
||||
name: leaf-103
|
||||
vpc_domain: vpcdom-siteA
|
||||
csw: {name: CSW-A, asn: 65100, peer_ip: 10.100.0.1, local_ip: 10.100.0.2, vlan: 100}
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: BadTenant
|
||||
stretch: false
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds: []
|
||||
aps: []
|
||||
contracts: []
|
||||
l3outs:
|
||||
- name: bad-l3out
|
||||
vrf: ghost
|
||||
border_leaves: [103]
|
||||
ext_epgs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_isn_full_mesh_single_site_raises(self) -> None:
|
||||
"""isn.peer_mesh='full' with fewer than 2 sites must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: true
|
||||
peer_mesh: full
|
||||
tenants: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_l3out_references_missing_border_leaf_raises(self) -> None:
|
||||
"""L3Out.border_leaves id not present in any site's border_leaves must raise."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
border_leaves:
|
||||
- id: 103
|
||||
name: leaf-103
|
||||
vpc_domain: vpcdom-siteA
|
||||
csw: {name: CSW-A, asn: 65100, peer_ip: 10.100.0.1, local_ip: 10.100.0.2, vlan: 100}
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: T
|
||||
stretch: false
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds: []
|
||||
aps: []
|
||||
contracts: []
|
||||
l3outs:
|
||||
- name: l3o
|
||||
vrf: vrf1
|
||||
border_leaves: [999]
|
||||
ext_epgs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_fault_seed_unknown_node_raises(self) -> None:
|
||||
"""faults.seed[i].node that is not a real node id must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants: []
|
||||
faults:
|
||||
seed:
|
||||
- severity: minor
|
||||
code: "F0532"
|
||||
node: 9999
|
||||
descr: "fault on a non-existent node"
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_epg_references_missing_contract_raises(self) -> None:
|
||||
"""EPG contract ref not present in tenant.contracts must raise ValidationError."""
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 1
|
||||
leaves: 1
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants:
|
||||
- name: T
|
||||
stretch: false
|
||||
sites: [SiteA]
|
||||
vrfs:
|
||||
- name: vrf1
|
||||
bds:
|
||||
- name: bd1
|
||||
vrf: vrf1
|
||||
subnets: []
|
||||
aps:
|
||||
- name: App
|
||||
epgs:
|
||||
- name: epg1
|
||||
bd: bd1
|
||||
contracts:
|
||||
provide: [ghost-contract]
|
||||
consume: []
|
||||
contracts: []
|
||||
l3outs: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
|
||||
def test_border_leaf_id_collision_raises(self) -> None:
|
||||
"""A border-leaf id colliding with an auto-generated leaf id must raise."""
|
||||
# SiteA auto leaves are 101,102; giving a border leaf id=101 collides.
|
||||
bad_yaml = """
|
||||
fabric:
|
||||
name: bad-topo
|
||||
sites:
|
||||
- name: SiteA
|
||||
id: "1"
|
||||
apic_host: "127.0.0.1:8443"
|
||||
asn: 65001
|
||||
pod: 1
|
||||
spines: 2
|
||||
leaves: 2
|
||||
border_leaves:
|
||||
- id: 101
|
||||
name: leaf-101-dup
|
||||
vpc_domain: vpcdom-siteA
|
||||
csw: {name: CSW-A, asn: 65100, peer_ip: 10.100.0.1, local_ip: 10.100.0.2, vlan: 100}
|
||||
cabling: auto
|
||||
isn:
|
||||
enabled: false
|
||||
tenants: []
|
||||
"""
|
||||
with pytest.raises(ValidationError):
|
||||
self._load_bad(bad_yaml)
|
||||
Reference in New Issue
Block a user