mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Add visual dual-verdict editor for monitor templates with out_of_expect and field tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2c2768a3d0
commit
27c6b7c61d
7 changed files with 985 additions and 136 deletions
|
|
@ -119,7 +119,59 @@ def side_verdict(
|
|||
return "ok", "gray"
|
||||
|
||||
|
||||
def _side_tokens(kind: str, status: str) -> set[str]:
|
||||
def field_token(field: str, value: str) -> str:
|
||||
"""Canonical field token used in success patterns: field:<name>:<value>."""
|
||||
return f"field:{str(field).strip().lower()}:{str(value).strip().lower()}"
|
||||
|
||||
|
||||
def _fields_for_tokens(
|
||||
sheet_override: dict[str, Any] | None,
|
||||
success_patterns: list[dict[str, Any]] | None,
|
||||
) -> set[str]:
|
||||
"""Fields whose current values should be emitted as field:* tokens."""
|
||||
ov = sheet_override or {}
|
||||
fields: set[str] = set()
|
||||
for f in ov.get("status_fields") or []:
|
||||
s = str(f or "").strip()
|
||||
if s:
|
||||
fields.add(s)
|
||||
for ft in ov.get("field_tokens") or []:
|
||||
if isinstance(ft, dict):
|
||||
s = str(ft.get("field") or "").strip()
|
||||
if s:
|
||||
fields.add(s)
|
||||
for pat in success_patterns or []:
|
||||
if not isinstance(pat, dict):
|
||||
continue
|
||||
for side in ("old", "new"):
|
||||
for tok in pat.get(side) or []:
|
||||
t = str(tok or "")
|
||||
if t.startswith("field:") and t.count(":") >= 2:
|
||||
# field:name:value
|
||||
parts = t.split(":", 2)
|
||||
if parts[1]:
|
||||
fields.add(parts[1])
|
||||
return fields
|
||||
|
||||
|
||||
def _field_tokens_from_row(row: dict[str, Any] | None, fields: set[str]) -> set[str]:
|
||||
if not row or not fields:
|
||||
return set()
|
||||
out: set[str] = set()
|
||||
for f in fields:
|
||||
v = str(row.get(f) or "").strip()
|
||||
if v:
|
||||
out.add(field_token(f, v))
|
||||
return out
|
||||
|
||||
|
||||
def _side_tokens(
|
||||
kind: str,
|
||||
status: str,
|
||||
*,
|
||||
row: dict[str, Any] | None = None,
|
||||
fields: set[str] | None = None,
|
||||
) -> set[str]:
|
||||
toks: set[str] = set()
|
||||
k = str(kind or "").strip()
|
||||
if k:
|
||||
|
|
@ -127,6 +179,7 @@ def _side_tokens(kind: str, status: str) -> set[str]:
|
|||
s = str(status or "").strip()
|
||||
if s and s != "none":
|
||||
toks.add(s)
|
||||
toks |= _field_tokens_from_row(row, fields or set())
|
||||
return toks
|
||||
|
||||
|
||||
|
|
@ -147,6 +200,28 @@ def _match_success(
|
|||
return False
|
||||
|
||||
|
||||
def _match_field_token_rules(
|
||||
old_row: dict[str, Any] | None,
|
||||
new_row: dict[str, Any] | None,
|
||||
field_tokens: list[dict[str, Any]] | None,
|
||||
) -> bool:
|
||||
"""Optional AND constraints: each {side, field, in:[...]} must hold."""
|
||||
rules = [r for r in (field_tokens or []) if isinstance(r, dict)]
|
||||
if not rules:
|
||||
return True
|
||||
for ft in rules:
|
||||
side = str(ft.get("side") or "").strip().lower()
|
||||
field = str(ft.get("field") or "").strip()
|
||||
allowed = {str(x).strip().lower() for x in (ft.get("in") or []) if str(x).strip()}
|
||||
if not field or not allowed:
|
||||
continue
|
||||
row = old_row if side == "old" else new_row if side == "new" else None
|
||||
val = str((row or {}).get(field) or "").strip().lower()
|
||||
if val not in allowed:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def dual_verdict(
|
||||
*,
|
||||
old_kind: str,
|
||||
|
|
@ -157,6 +232,10 @@ def dual_verdict(
|
|||
old_status: str = "none",
|
||||
new_status: str = "none",
|
||||
success_patterns: list[dict[str, Any]] | None = None,
|
||||
out_of_expect: str = "strict",
|
||||
old_row: dict[str, Any] | None = None,
|
||||
new_row: dict[str, Any] | None = None,
|
||||
sheet_override: dict[str, Any] | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Synthesize old+new into migration board verdict.
|
||||
|
||||
|
|
@ -164,21 +243,28 @@ def dual_verdict(
|
|||
(``unfinished`` / ``lost``), not yellow migrating.
|
||||
|
||||
When ``success_patterns`` is set (from monitor sheet_overrides), tokens may
|
||||
include kinds and status classes (up/down) so e.g. old down + new up → migrated.
|
||||
include kinds, status classes (up/down), and ``field:name:value``.
|
||||
|
||||
``out_of_expect``: strict (default) | warn (yellow instead of red) | ignore.
|
||||
"""
|
||||
if in_expect and _match_success(
|
||||
_side_tokens(old_kind, old_status),
|
||||
_side_tokens(new_kind, new_status),
|
||||
success_patterns,
|
||||
):
|
||||
ov = sheet_override or {}
|
||||
fields = _fields_for_tokens(ov, success_patterns)
|
||||
old_toks = _side_tokens(old_kind, old_status, row=old_row, fields=fields)
|
||||
new_toks = _side_tokens(new_kind, new_status, row=new_row, fields=fields)
|
||||
field_ok = _match_field_token_rules(old_row, new_row, list(ov.get("field_tokens") or []) or None)
|
||||
|
||||
if in_expect and field_ok and _match_success(old_toks, new_toks, success_patterns):
|
||||
return "migrated", "green"
|
||||
|
||||
ooe = str(out_of_expect or "strict").strip().lower()
|
||||
if not in_expect:
|
||||
if ooe == "ignore":
|
||||
return "not_involved", "gray"
|
||||
if old_kind in ("removed", "changed") or new_kind in ("removed", "changed"):
|
||||
if old_kind == "removed" and new_kind in ("", "removed"):
|
||||
return "lost", "red"
|
||||
return ("lost", "yellow") if ooe == "warn" else ("lost", "red")
|
||||
if old_kind in ("removed", "changed") or new_kind in ("changed",):
|
||||
return "anomaly", "red"
|
||||
return ("anomaly", "yellow") if ooe == "warn" else ("anomaly", "red")
|
||||
if old_kind == "added" or new_kind == "added":
|
||||
return "unexpected_new", "yellow"
|
||||
return "not_involved", "gray"
|
||||
|
|
@ -195,12 +281,12 @@ def dual_verdict(
|
|||
if acceptance:
|
||||
return "unfinished", "red"
|
||||
return "migrating", "yellow"
|
||||
ov, oc = side_verdict(kind=old_kind or "unchanged", in_expect=True, window_active=window_active)
|
||||
if oc == "red" or (new_kind in ("removed",) and old_kind != "removed"):
|
||||
side_v, side_c = side_verdict(kind=old_kind or "unchanged", in_expect=True, window_active=window_active)
|
||||
if side_c == "red" or (new_kind in ("removed",) and old_kind != "removed"):
|
||||
return "anomaly", "red"
|
||||
if acceptance:
|
||||
return "unfinished", "red"
|
||||
if window_active or ov.startswith("expected"):
|
||||
if window_active or side_v.startswith("expected"):
|
||||
return "migrating", "yellow"
|
||||
return "migrating", "yellow"
|
||||
|
||||
|
|
@ -280,10 +366,22 @@ def evaluate_metric_dual(
|
|||
acceptance: bool = False,
|
||||
field_rules: list[dict[str, Any]] | None = None,
|
||||
sheet_override: dict[str, Any] | None = None,
|
||||
out_of_expect: str = "strict",
|
||||
) -> dict[str, Any]:
|
||||
"""Run old vs old-baseline, new vs new-baseline (or mapped old baseline), dual merge."""
|
||||
expect_keys = expect_keys_for_metric(expect, metric_id=metric_id, iface_fields=iface_fields)
|
||||
ov = sheet_override or {}
|
||||
if ov.get("skip_dual"):
|
||||
return {
|
||||
"metric_id": metric_id,
|
||||
"old_summary": {},
|
||||
"new_summary": {},
|
||||
"progress_ok": 0,
|
||||
"progress_total": 0,
|
||||
"anomaly": 0,
|
||||
"rows": [],
|
||||
"skipped": True,
|
||||
}
|
||||
success_patterns = list(ov.get("success") or []) if isinstance(ov.get("success"), list) else []
|
||||
|
||||
old_cmp = compare_rows(
|
||||
|
|
@ -372,6 +470,10 @@ def evaluate_metric_dual(
|
|||
old_status=old_st,
|
||||
new_status=new_st,
|
||||
success_patterns=success_patterns or None,
|
||||
out_of_expect=out_of_expect,
|
||||
old_row=old_cur or None,
|
||||
new_row=new_cur or None,
|
||||
sheet_override=ov,
|
||||
)
|
||||
if in_exp and verdict == "migrated" and color == "green":
|
||||
progress_ok += 1
|
||||
|
|
|
|||
|
|
@ -67,6 +67,58 @@ def ensure_port_compare_template(db: Session) -> BizCompareTemplate:
|
|||
return row
|
||||
|
||||
|
||||
def preset_override_for_metric(metric_id: str) -> dict[str, Any]:
|
||||
"""Default dual-verdict overlay for a compare-template sheet."""
|
||||
mid = str(metric_id or "").strip()
|
||||
if mid == PORT_METRIC_ID:
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": list(PORT_STATUS_FIELDS),
|
||||
"down_values": ["down"],
|
||||
"up_values": ["up"],
|
||||
"success": [{"old": ["removed", "down"], "new": ["added", "up", "unchanged"]}],
|
||||
}
|
||||
if mid == "bgp_peer":
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": ["state"],
|
||||
"down_values": ["idle", "active", "connect", "down"],
|
||||
"up_values": ["established"],
|
||||
"success": [{"old": ["removed", "down"], "new": ["added", "up", "unchanged"]}],
|
||||
}
|
||||
if mid in ("arp", "nd6_cache", "lldp_neighbor"):
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": [],
|
||||
"down_values": [],
|
||||
"up_values": [],
|
||||
"success": [{"old": ["removed"], "new": ["added", "unchanged"]}],
|
||||
}
|
||||
if "isis" in mid or "ospf" in mid or "adjacency" in mid:
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": ["state"],
|
||||
"down_values": ["down", "init", "idle"],
|
||||
"up_values": ["up", "full", "2way"],
|
||||
"success": [{"old": ["removed", "down"], "new": ["added", "up", "unchanged"]}],
|
||||
}
|
||||
if "route" in mid or "vrf" in mid:
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": [],
|
||||
"down_values": [],
|
||||
"up_values": [],
|
||||
"success": [{"old": ["removed"], "new": ["added", "unchanged"]}],
|
||||
}
|
||||
return {
|
||||
"metric_id": mid,
|
||||
"status_fields": [],
|
||||
"down_values": ["down"],
|
||||
"up_values": ["up"],
|
||||
"success": [{"old": ["removed"], "new": ["added", "unchanged"]}],
|
||||
}
|
||||
|
||||
|
||||
def ensure_default_monitor_templates(db: Session) -> None:
|
||||
"""Seed once when monitor-template table is empty."""
|
||||
if db.query(BizMonitorTemplate.id).limit(1).first():
|
||||
|
|
@ -85,26 +137,15 @@ def ensure_default_monitor_templates(db: Session) -> None:
|
|||
compare_template_id=port_tpl.id,
|
||||
collect_metric_ids_json=[PORT_METRIC_ID],
|
||||
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||
sheet_overrides_json=[
|
||||
{
|
||||
"metric_id": PORT_METRIC_ID,
|
||||
"status_fields": list(PORT_STATUS_FIELDS),
|
||||
"down_values": ["down"],
|
||||
"up_values": ["up"],
|
||||
"success": [
|
||||
{
|
||||
"old": ["removed", "down"],
|
||||
"new": ["added", "up", "unchanged"],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
sheet_overrides_json=[preset_override_for_metric(PORT_METRIC_ID)],
|
||||
note="Default: port status dual-verdict with up/down semantics",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
),
|
||||
]
|
||||
if zte:
|
||||
zte_sheets = cmp_svc.template_metrics(zte)
|
||||
zte_overrides = [preset_override_for_metric(str(s.get("metric_id") or "")) for s in zte_sheets if s.get("metric_id")]
|
||||
seeds.append(
|
||||
BizMonitorTemplate(
|
||||
id=uuid4().hex,
|
||||
|
|
@ -112,8 +153,8 @@ def ensure_default_monitor_templates(db: Session) -> None:
|
|||
compare_template_id=zte.id,
|
||||
collect_metric_ids_json=[],
|
||||
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||
sheet_overrides_json=[],
|
||||
note="Uses ZTE status compare template; presence dual-verdict",
|
||||
sheet_overrides_json=zte_overrides,
|
||||
note="ZTE multi-sheet: port/ARP/BGP/… dual presets",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -360,7 +360,8 @@ def run_evaluate(
|
|||
# Final acceptance: window closed → unfinished expect = red
|
||||
window_active = (mb.status == "active") and (not acceptance)
|
||||
mt = resolve_project_monitor_template(db, proj)
|
||||
sheets, sheet_overrides, _defaults = resolve_evaluate_sheets(db, mt)
|
||||
sheets, sheet_overrides, defaults = resolve_evaluate_sheets(db, mt)
|
||||
out_of_expect = str((defaults or {}).get("out_of_expect") or "strict").strip().lower()
|
||||
|
||||
sheet_cards: list[dict[str, Any]] = []
|
||||
all_rows: list[dict[str, Any]] = []
|
||||
|
|
@ -377,6 +378,8 @@ def run_evaluate(
|
|||
row_filters = list(sheet.get("row_filters") or [])
|
||||
field_rules = list(sheet.get("field_rules") or [])
|
||||
sheet_ov = override_for_metric(sheet_overrides, mid)
|
||||
if sheet_ov.get("skip_dual"):
|
||||
continue
|
||||
|
||||
old_base = apply_row_filters(
|
||||
_load_metric_rows(db, batch_id=proj.old_baseline_batch_id, metric_id=mid),
|
||||
|
|
@ -412,6 +415,7 @@ def run_evaluate(
|
|||
acceptance=acceptance,
|
||||
field_rules=field_rules,
|
||||
sheet_override=sheet_ov,
|
||||
out_of_expect=out_of_expect,
|
||||
)
|
||||
sheet_cards.append(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue