mirror of
https://github.com/dtzp555-max/aci-sim.git
synced 2026-07-21 21:15:12 +00:00
feat(validation): server-side MO value validation — real-APIC 400 on bad values (v0.23.0)
Closes F10: the sim silently stored out-of-range/malformed scalar values
(e.g. vlan-9999, IP 999.888.777.x) that real APIC/NDO reject server-side.
New aci_sim/rest_aci/validators.py: table-driven RULES {(class,prop) -> validator}
(range/enum/IP·MAC-format/vlan-encap). Enforced in writes.py::_validate_planned
(APIC) and ndo/patch.py::apply_json_patch (NDO) before any store mutation.
Fail-safe default-allow: only registered (class,prop) validated; unknown pass.
Bad values now -> APIC Error 103 Malformed MO body. ~45 rules / ~18 classes.
Constraints sourced from vendored cisco.aci arg-specs + standard ACI ranges,
cross-checked against the real-machine var corpus for zero false-rejects
(regression: full create_tenant MS-TN1 + SF-TN1 chains stay green). +176 tests
(1098 passed). Referential integrity split out as F11 (needs real-APIC study).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zdUTqU9fvCCvF3uVGZsu1
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ffaf9964c2
commit
fe3d13221a
+25
-28
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from aci_sim.build.fabric import loopback_ip, oob_ip
|
||||
@@ -11,6 +10,7 @@ from aci_sim.build.neighbors import add_switch_adjacency
|
||||
from aci_sim.mit.dn import pod_from_dn
|
||||
from aci_sim.mit.mo import MO
|
||||
from aci_sim.mit.store import MITStore
|
||||
from aci_sim.rest_aci.validators import RULES
|
||||
from aci_sim.topology.schema import Node
|
||||
|
||||
# Class -> RN template for children POSTed without an explicit ``dn``.
|
||||
@@ -215,36 +215,33 @@ def _validate_node(cls: str, body: dict, path: str) -> None:
|
||||
|
||||
|
||||
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)."""
|
||||
"""Reject malformed/out-of-range property values a real APIC would 400 on
|
||||
(error-107/801 style) — e.g. ``fvSubnet ip=".1/24"``,
|
||||
``l3extRsPathL3OutAtt encap="vlan-9999"``, ``bgpAsP asn="0"``. 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).
|
||||
|
||||
Table-driven (F10): every ``(class, prop)`` pair checked here comes from
|
||||
``aci_sim.rest_aci.validators.RULES`` — see that module + design doc
|
||||
``_F10_VALIDATION_DESIGN.md`` §3.2/§3.6 for the registry and its
|
||||
fail-safe default-allow policy. A ``(class, prop)`` pair with NO entry in
|
||||
RULES is intentionally NOT validated (unknown ⇒ allow, never reject)."""
|
||||
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
|
||||
for prop, value in mo_attrs.items():
|
||||
validator = RULES.get((mo_cls, prop))
|
||||
if validator is None or value is None:
|
||||
continue # fail-safe: unregistered (class, prop) or unset value ⇒ allow
|
||||
try:
|
||||
validator(mo_cls, prop, value, mo_attrs)
|
||||
except ValueError as reason:
|
||||
raise WriteValidationError(
|
||||
f"Invalid value {value!r} for property {prop!r} of {mo_cls} "
|
||||
f"{mo_attrs.get('dn', '')!r}: {reason}"
|
||||
) from None
|
||||
|
||||
|
||||
def _plan_recursive(cls: str, attrs: dict, children: list, planned: list[tuple[str, dict]]) -> None:
|
||||
|
||||
Reference in New Issue
Block a user