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
@@ -28,6 +28,8 @@ import hashlib
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aci_sim.rest_aci.validators import Validator, fmt, rng
|
||||
|
||||
#: Every collection key a schema template can carry. Real NDO always serves
|
||||
#: these back as (at minimum) empty lists even when a caller's create/add
|
||||
#: payload omitted them — e.g. `mso_schema_template.py`'s "schema does not
|
||||
@@ -81,6 +83,77 @@ class PatchError(Exception):
|
||||
"""Raised when a JSON-Patch op cannot be applied to the stored document."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# F10 batch 3 — NDO value validation (design doc §3.3).
|
||||
#
|
||||
# NDO stores schema JSON, not APIC MOs, so values arrive as JSON-Patch
|
||||
# add/replace op values rather than (class, prop) MO attrs — the RULES
|
||||
# registry keying in aci_sim/rest_aci/validators.py doesn't apply verbatim.
|
||||
# Instead NDO_RULES keys on (collection, field): *collection* is the name of
|
||||
# the array a value is being written into/under (e.g. "subnets",
|
||||
# "staticPorts" — the dict key one level up from the array itself), *field*
|
||||
# is the attribute name within that array's object shape.
|
||||
#
|
||||
# The NDO value surface is much narrower than APIC's (design §3.3: "subnet
|
||||
# ip, static-port vlan, a few vrf/bd enums") — this batch implements exactly
|
||||
# the two the design doc calls out by name: subnet ip and static-port VLAN.
|
||||
# The real `cisco.mso` field for the static-port VLAN is `portEncapVlan`
|
||||
# (confirmed against this repo's own PR-12 test fixtures,
|
||||
# tests/test_pr12_site_local.py — the design doc's pseudocode used the
|
||||
# placeholder name "vlan"; `portEncapVlan` is the actual wire field).
|
||||
# ---------------------------------------------------------------------------
|
||||
NDO_RULES: dict[tuple[str, str], Validator] = {
|
||||
("subnets", "ip"): fmt("ipv4_iface"),
|
||||
("staticPorts", "portEncapVlan"): rng(1, 4094),
|
||||
}
|
||||
|
||||
|
||||
def _validate_ndo_value(collection: str, field: str, value: Any) -> None:
|
||||
"""Look up *collection*/*field* in NDO_RULES and raise PatchError if
|
||||
*value* fails it. Fail-safe default-allow, same policy as the APIC
|
||||
registry: an unregistered (collection, field) or a None value is not
|
||||
validated."""
|
||||
validator = NDO_RULES.get((collection, field))
|
||||
if validator is None or value is None:
|
||||
return
|
||||
try:
|
||||
validator(collection, field, value, None)
|
||||
except ValueError as reason:
|
||||
raise PatchError(
|
||||
f"Invalid value {value!r} for property {field!r} of {collection!r}: {reason}"
|
||||
) from None
|
||||
|
||||
|
||||
def _validate_ndo_op(tokens: list[str], container: Any, last: str, value: Any) -> None:
|
||||
"""Validate a single JSON-Patch add/replace op's *value* against
|
||||
NDO_RULES, given the already-resolved (container, last) from
|
||||
``_resolve_container`` and the op's full *tokens* path.
|
||||
|
||||
Two value shapes reach here (design §3.3 "applied to each op's value"):
|
||||
|
||||
- Whole-object add/insert (``container`` is the collection list
|
||||
itself, e.g. ``.../subnets/-`` or ``.../staticPorts/-``): *value* is
|
||||
a dict of fields for the new item. The collection name is the token
|
||||
that named this list, i.e. ``tokens[-2]`` (the list was reached by
|
||||
indexing into ``doc[...][tokens[-2]]``). Validate every field of
|
||||
*value* that NDO_RULES knows about.
|
||||
- Single-field replace/add on an EXISTING item (``container`` is that
|
||||
item's own dict, e.g. ``.../subnets/0/ip``): *last* IS the field
|
||||
name and *value* is the scalar. The collection name is one level up
|
||||
again — ``tokens[-3]`` (the list containing the item ``tokens[-2]``
|
||||
indexes into).
|
||||
"""
|
||||
if isinstance(container, list):
|
||||
collection = tokens[-2] if len(tokens) >= 2 else None
|
||||
if collection is not None and isinstance(value, dict):
|
||||
for field, sub_value in value.items():
|
||||
_validate_ndo_value(collection, field, sub_value)
|
||||
elif isinstance(container, dict):
|
||||
collection = tokens[-3] if len(tokens) >= 3 else None
|
||||
if collection is not None:
|
||||
_validate_ndo_value(collection, last, value)
|
||||
|
||||
|
||||
def _normalize_object(obj: dict, array_key: str, schema_id: str, template_name: str, anp_name: str = "") -> None:
|
||||
"""Backfill *obj*'s missing collection-default keys for its container
|
||||
array (e.g. array_key="bds" -> obj gets subnets/dhcpLabels defaults),
|
||||
@@ -695,6 +768,12 @@ def apply_json_patch(doc: dict, ops: list[dict]) -> dict:
|
||||
|
||||
container, last = _resolve_container(doc, tokens)
|
||||
|
||||
if op in ("add", "replace"):
|
||||
# F10 batch 3: validate before mutating (design §3.3) — a bad
|
||||
# subnet ip / static-port vlan 400s instead of landing in the
|
||||
# stored schema doc.
|
||||
_validate_ndo_op(tokens, container, last, value)
|
||||
|
||||
if op == "add":
|
||||
if isinstance(container, list):
|
||||
if last == "-":
|
||||
|
||||
Reference in New Issue
Block a user