mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-21 21:15:12 +00:00
feat: APIC-style write validation, query-target=children, contract shadow mirroring (v0.21.0)
Three fidelity gaps surfaced by one broken TN2 var file (missing firewall block), all fixed: - writes: reject malformed fvSubnet/l3extSubnet/vnsRedirectDest ip values with an APIC-style 400 before any store mutation (deletes exempt) - query engine: support query-target=children (direct children, flat imdata, root excluded) — was silently returning empty - deploy mirror: materialize vzFilter/vzEntry, vzBrCP/vzSubj (+ filter and service-graph subject bindings) and vnsAbsGraph shadows per target site; undeploy removes them E2E-verified through the real pipeline (aci-py hidden push): a TN2 var missing the fw block now fails with 'Invalid value "." for property ip of vnsRedirectDest ... -> 400' and nothing lands; a complete var set builds both MS tenants with contract shadows + sgt-FW graph binding on both sites. Suite: 914 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7fdf31aca9
commit
867c05c9c5
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from aci_sim.build.fabric import loopback_ip, oob_ip
|
||||
@@ -213,6 +214,39 @@ def _validate_node(cls: str, body: dict, path: str) -> None:
|
||||
_validate_node(child_cls, child_body, child_path)
|
||||
|
||||
|
||||
def _validate_planned(planned: list[tuple[str, dict]]) -> None:
|
||||
"""Reject malformed property values a real APIC would 400 on (error-801
|
||||
style) — e.g. ``fvSubnet ip=".1/24"`` or ``vnsRedirectDest ip="."``, the
|
||||
exact garbage an unguarded J2 template renders when its input vars are
|
||||
missing. The sim's contract is to FAIL the way real gear fails, not to
|
||||
absorb it into the MIT. Only values actually PRESENT on a non-delete
|
||||
write are validated (a delete needs nothing but the DN — that stays the
|
||||
cleanup path for anything malformed that predates this check)."""
|
||||
for mo_cls, mo_attrs in planned:
|
||||
if mo_attrs.get("status") == "deleted":
|
||||
continue
|
||||
if mo_cls in ("fvSubnet", "l3extSubnet"):
|
||||
ip = mo_attrs.get("ip")
|
||||
if ip is not None:
|
||||
try:
|
||||
ipaddress.ip_interface(ip)
|
||||
except ValueError:
|
||||
raise WriteValidationError(
|
||||
f"Invalid value {ip!r} for property 'ip' of {mo_cls} "
|
||||
f"{mo_attrs.get('dn', '')!r}: not a valid address[/prefix]"
|
||||
) from None
|
||||
elif mo_cls == "vnsRedirectDest":
|
||||
ip = mo_attrs.get("ip")
|
||||
if ip is not None:
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
raise WriteValidationError(
|
||||
f"Invalid value {ip!r} for property 'ip' of vnsRedirectDest "
|
||||
f"{mo_attrs.get('dn', '')!r}: not a valid IP address"
|
||||
) from None
|
||||
|
||||
|
||||
def _plan_recursive(cls: str, attrs: dict, children: list, planned: list[tuple[str, dict]]) -> None:
|
||||
"""Build the ordered list of (class, attrs) MOs to write, without touching the store.
|
||||
|
||||
@@ -253,6 +287,7 @@ def _upsert_recursive(store: MITStore, cls: str, attrs: dict, children: list) ->
|
||||
|
||||
planned: list[tuple[str, dict]] = []
|
||||
_plan_recursive(cls, attrs, children, planned)
|
||||
_validate_planned(planned) # reject malformed values BEFORE any store mutation
|
||||
|
||||
# Validation passed for the entire subtree — now, and only now, mutate
|
||||
# the store (400 on validation failure => zero side effects).
|
||||
|
||||
Reference in New Issue
Block a user