mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 23:40:54 +08:00
Correctness (A): normalize port-map names with the template rules (snapshot v2, v1 replays unchanged); protect mappings/templates that are referenced; stop dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated; validate baseline ownership on project create; one canonical key form; fix built-in template tolerances and upgrade old packaged values in place. Cross-vendor identity (B): single identity pipeline for every path, per-side and per-vendor interface rules, value maps, filters after normalization, mapping pre-flight for compare and cutover projects, SQL eligibility fixes. Data quality (V): no_data / suspect_parse / not_collected sheet states, command slice coverage, directional compare modes, value-sorted duplicate pairing, before-side raw names in results and CSV. Robustness (H): bounded auto-run history, background evaluate/finish with polling, manual route tables out of the acceptance gate, baseline row cache, scoped duplicate-key failures, merged HF tasks with a per-NE session cap, same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red tickets. Frontend: pre-flight report, mapping table view, template editor support for the new rules, data-state banners. Docs: canonical field contract. Not included: Huawei status parsers (no real samples), SQL-path route compare. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
72 lines
4 KiB
Python
72 lines
4 KiB
Python
"""Validate monitor correction rules before they can affect live verdicts."""
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from .evaluate import _groups_for_side, _normalize_cond_type
|
|
|
|
|
|
def validate_monitor_rules(defaults: Any, overrides: Any) -> None:
|
|
def fail(path: str) -> None:
|
|
raise HTTPException(status_code=400, detail=f"monitor_rule_invalid:{path}")
|
|
|
|
if not isinstance(defaults, dict) or not isinstance(overrides, list):
|
|
fail("defaults/sheet_overrides")
|
|
if defaults.get("out_of_expect", "strict") not in ("strict", "warn", "ignore"):
|
|
fail("defaults.out_of_expect")
|
|
seen = set()
|
|
for i, ov in enumerate(overrides):
|
|
path = f"sheet_overrides[{i}]"
|
|
if not isinstance(ov, dict) or not (ov.get("sheet_id") or ov.get("metric_id")):
|
|
fail(path)
|
|
ident = ov.get("sheet_id") or ov.get("metric_id")
|
|
if ident in seen:
|
|
fail(path + ".duplicate")
|
|
seen.add(ident)
|
|
if ov.get("scope_by_port_map") is not None and not isinstance(ov["scope_by_port_map"], bool):
|
|
fail(path + ".scope_by_port_map")
|
|
for key in ("status_fields", "up_values", "down_values"):
|
|
if key in ov and (not isinstance(ov[key], list) or any(not str(v).strip() for v in ov[key])):
|
|
fail(path + "." + key)
|
|
for key in ("success", "anomaly"):
|
|
patterns = ov.get(key, [])
|
|
if not isinstance(patterns, list):
|
|
fail(path + "." + key)
|
|
for j, pat in enumerate(patterns):
|
|
pp = f"{path}.{key}[{j}]"
|
|
if not isinstance(pat, dict):
|
|
fail(pp)
|
|
for side in ("old", "new"):
|
|
for attr in (side, side + "_conds", side + "_groups"):
|
|
if attr in pat and not isinstance(pat[attr], list):
|
|
fail(pp + "." + attr)
|
|
if any(isinstance(g, list) and not g for g in pat.get(side + "_groups", [])):
|
|
fail(pp + "." + side + "_groups.empty_group")
|
|
groups = _groups_for_side(pat, side)
|
|
if key == "success" and not groups:
|
|
fail(pp + "." + side + ".required")
|
|
for group in groups:
|
|
for cond in group:
|
|
cp = pp + "." + side
|
|
if isinstance(cond, str):
|
|
if cond not in ("added", "removed", "changed", "unchanged", "up", "down", "other", "none") and not (cond.startswith("field:") and cond.count(":") >= 2):
|
|
fail(cp + ".token")
|
|
elif isinstance(cond, dict):
|
|
typ = _normalize_cond_type(cond)
|
|
if typ not in ("presence", "status", "value"):
|
|
fail(cp + ".type")
|
|
val = cond.get("value", cond.get("kind", cond.get("status")))
|
|
if typ == "presence" and val not in ("added", "removed", "changed", "unchanged"):
|
|
fail(cp + ".presence")
|
|
if typ == "status" and val not in ("up", "down", "other", "none"):
|
|
fail(cp + ".status")
|
|
if typ == "value":
|
|
op = cond.get("op", "eq")
|
|
if op not in ("eq", "ne", "in", "not_in", "empty", "not_empty", "changed", "unchanged", "same", "diff"):
|
|
fail(cp + ".op")
|
|
if op in ("eq", "ne", "in", "not_in") and (val is None or val == "" or val == []):
|
|
fail(cp + ".value")
|
|
else:
|
|
fail(cp + ".condition")
|
|
if key == "anomaly" and not any(_groups_for_side(pat, s) for s in ("old", "new")):
|
|
fail(pp + ".empty")
|