mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 00:32:39 +08:00
Decouple cutover HF from portrait and tighten ops UI.
Portrait stays on biz-monitor; cutover create/ensure owns purpose=cutover_hf slots with project collect override, multi-interval bindings, window pause, and industrial detail UX. Also expand ZTE collect/config parsers and TextFSM for route/BGP/L2VPN/VRRP coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a65c9fc420
commit
285c19e5a3
67 changed files with 9510 additions and 723 deletions
|
|
@ -796,11 +796,21 @@ def _key_in_port_map(
|
|||
iface_fields: list[str],
|
||||
port_map: dict[str, str],
|
||||
) -> bool:
|
||||
"""True when every interface segment of the key is an old-side map entry."""
|
||||
"""True when every interface segment of the key is covered by the map.
|
||||
|
||||
Covered = exact map key, or parent (before last ``.``) is a map key.
|
||||
"""
|
||||
vals = _iface_values(key_str, key_fields=key_fields, iface_fields=iface_fields)
|
||||
if not vals:
|
||||
return False
|
||||
return all(v in port_map for v in vals)
|
||||
for v in vals:
|
||||
if v in port_map:
|
||||
continue
|
||||
parent = v.rsplit(".", 1)[0] if "." in v else ""
|
||||
if parent and parent in port_map:
|
||||
continue
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _port_identity(key_fields: list[str], iface_fields: list[str]) -> bool:
|
||||
|
|
@ -832,24 +842,28 @@ def _remap_key_str(
|
|||
port_map: dict[str, str],
|
||||
) -> str:
|
||||
"""Map old-side key_str → new-side using iface segments (not whole-string only)."""
|
||||
from ..biz_state.iface_normalize import resolve_mapped_iface
|
||||
|
||||
if not key_str:
|
||||
return key_str
|
||||
if not port_map:
|
||||
return key_str
|
||||
if key_str in port_map and (not key_fields or len(key_fields) == 1):
|
||||
return port_map[key_str]
|
||||
if (not key_fields or len(key_fields) == 1) and (
|
||||
key_str in port_map or ("." in key_str and key_str.rsplit(".", 1)[0] in port_map)
|
||||
):
|
||||
return resolve_mapped_iface(key_str, port_map)
|
||||
parts = str(key_str).split("|")
|
||||
if key_fields and len(parts) == len(key_fields):
|
||||
iface_set = {str(f) for f in iface_fields}
|
||||
out: list[str] = []
|
||||
for i, f in enumerate(key_fields):
|
||||
v = parts[i]
|
||||
if f in iface_set and v in port_map:
|
||||
out.append(port_map[v])
|
||||
if f in iface_set:
|
||||
out.append(resolve_mapped_iface(v, port_map))
|
||||
else:
|
||||
out.append(v)
|
||||
return "|".join(out)
|
||||
return port_map.get(key_str, key_str)
|
||||
return resolve_mapped_iface(key_str, port_map)
|
||||
|
||||
|
||||
def _reverse_remap_key_str(
|
||||
|
|
@ -860,24 +874,28 @@ def _reverse_remap_key_str(
|
|||
rev_map: dict[str, str],
|
||||
) -> str:
|
||||
"""Map new-side key_str → old-side canon identity."""
|
||||
from ..biz_state.iface_normalize import resolve_mapped_iface
|
||||
|
||||
if not key_str:
|
||||
return key_str
|
||||
if not rev_map:
|
||||
return key_str
|
||||
if key_str in rev_map and (not key_fields or len(key_fields) == 1):
|
||||
return rev_map[key_str]
|
||||
if (not key_fields or len(key_fields) == 1) and (
|
||||
key_str in rev_map or ("." in key_str and key_str.rsplit(".", 1)[0] in rev_map)
|
||||
):
|
||||
return resolve_mapped_iface(key_str, rev_map)
|
||||
parts = str(key_str).split("|")
|
||||
if key_fields and len(parts) == len(key_fields):
|
||||
iface_set = {str(f) for f in iface_fields}
|
||||
out: list[str] = []
|
||||
for i, f in enumerate(key_fields):
|
||||
v = parts[i]
|
||||
if f in iface_set and v in rev_map:
|
||||
out.append(rev_map[v])
|
||||
if f in iface_set:
|
||||
out.append(resolve_mapped_iface(v, rev_map))
|
||||
else:
|
||||
out.append(v)
|
||||
return "|".join(out)
|
||||
return rev_map.get(key_str, key_str)
|
||||
return resolve_mapped_iface(key_str, rev_map)
|
||||
|
||||
|
||||
def _current_row(diff: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
|
@ -938,8 +956,14 @@ def evaluate_metric_dual(
|
|||
sheet_override: dict[str, Any] | None = None,
|
||||
out_of_expect: str = "strict",
|
||||
sheet_id: str = "",
|
||||
iface_normalize_rules: list[dict[str, str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run old vs old-baseline, new vs new-baseline (or mapped old baseline), dual merge."""
|
||||
from ..biz_state.iface_normalize import (
|
||||
apply_iface_normalize_rows,
|
||||
normalize_iface_rules,
|
||||
)
|
||||
|
||||
sid = str(sheet_id or "").strip() or str(metric_id or "").strip()
|
||||
expect_keys = expect_keys_for_metric(
|
||||
expect, metric_id=metric_id, iface_fields=iface_fields, sheet_id=sid
|
||||
|
|
@ -962,6 +986,21 @@ def evaluate_metric_dual(
|
|||
success_patterns = list(ov.get("success") or []) if isinstance(ov.get("success"), list) else []
|
||||
anomaly_patterns = list(ov.get("anomaly") or []) if isinstance(ov.get("anomaly"), list) else []
|
||||
|
||||
norm_rules = normalize_iface_rules(iface_normalize_rules)
|
||||
old_baseline_rows = apply_iface_normalize_rows(
|
||||
old_baseline_rows, iface_fields=iface_fields, rules=norm_rules
|
||||
)
|
||||
old_current_rows = apply_iface_normalize_rows(
|
||||
old_current_rows, iface_fields=iface_fields, rules=norm_rules
|
||||
)
|
||||
new_current_rows = apply_iface_normalize_rows(
|
||||
new_current_rows, iface_fields=iface_fields, rules=norm_rules
|
||||
)
|
||||
if new_baseline_rows is not None:
|
||||
new_baseline_rows = apply_iface_normalize_rows(
|
||||
new_baseline_rows, iface_fields=iface_fields, rules=norm_rules
|
||||
)
|
||||
|
||||
old_cmp = compare_rows(
|
||||
before_rows=old_baseline_rows,
|
||||
after_rows=old_current_rows,
|
||||
|
|
@ -970,6 +1009,7 @@ def evaluate_metric_dual(
|
|||
compare_fields=compare_fields,
|
||||
port_map=None,
|
||||
field_rules=field_rules,
|
||||
iface_normalize_rules=None, # already applied
|
||||
)
|
||||
old_idx = build_diff_index_from_compare(old_cmp)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
|||
from ..models import BizCompareTemplate, BizMonitorTemplate
|
||||
from ..timeutil import utcnow_naive
|
||||
from ..biz_state import compare_service as cmp_svc
|
||||
from ..biz_state.iface_normalize import default_zte_iface_normalize_rules
|
||||
from .evaluate import PORT_METRIC_ID, PORT_STATUS_FIELDS
|
||||
|
||||
|
||||
|
|
@ -18,13 +19,27 @@ def _utcnow():
|
|||
return utcnow_naive()
|
||||
|
||||
|
||||
def _out(row: BizMonitorTemplate, compare_name: str = "") -> dict[str, Any]:
|
||||
def _out(row: BizMonitorTemplate, compare_name: str = "", *, db: Session | None = None) -> dict[str, Any]:
|
||||
collect = [str(x).strip() for x in (row.collect_metric_ids_json or []) if str(x).strip()]
|
||||
effective = list(collect)
|
||||
if not effective and db is not None:
|
||||
ct = db.get(BizCompareTemplate, row.compare_template_id) if row.compare_template_id else None
|
||||
if ct:
|
||||
seen: list[str] = []
|
||||
for s in cmp_svc.template_metrics(ct):
|
||||
mid = str(s.get("metric_id") or "").strip()
|
||||
if mid and mid not in seen:
|
||||
seen.append(mid)
|
||||
effective = seen
|
||||
if not effective:
|
||||
effective = [PORT_METRIC_ID]
|
||||
return {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"compare_template_id": row.compare_template_id or "",
|
||||
"compare_template_name": compare_name,
|
||||
"collect_metric_ids": list(row.collect_metric_ids_json or []),
|
||||
"collect_metric_ids": collect,
|
||||
"collect_metric_ids_effective": effective,
|
||||
"defaults": dict(row.defaults_json or {}),
|
||||
"sheet_overrides": list(row.sheet_overrides_json or []),
|
||||
"note": row.note or "",
|
||||
|
|
@ -52,6 +67,11 @@ def ensure_port_compare_template(db: Session) -> BizCompareTemplate:
|
|||
"field_rules": [],
|
||||
}
|
||||
if row:
|
||||
if not cmp_svc.template_iface_normalize(row):
|
||||
cmp_svc._set_template_iface_normalize(row, default_zte_iface_normalize_rules()) # noqa: SLF001
|
||||
row.updated_at = _utcnow()
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
row = BizCompareTemplate(
|
||||
id=uuid4().hex,
|
||||
|
|
@ -61,6 +81,7 @@ def ensure_port_compare_template(db: Session) -> BizCompareTemplate:
|
|||
updated_at=_utcnow(),
|
||||
)
|
||||
cmp_svc._apply_sheets_to_row(row, [sheet]) # noqa: SLF001 — shared normalizer
|
||||
cmp_svc._set_template_iface_normalize(row, default_zte_iface_normalize_rules()) # noqa: SLF001
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
|
|
@ -262,68 +283,61 @@ def preset_override_for_metric(metric_id: str, *, sheet_id: str = "") -> dict[st
|
|||
)
|
||||
|
||||
|
||||
DEFAULT_MONITOR_TEMPLATE_NAME = "默认割接监控"
|
||||
|
||||
|
||||
def ensure_default_monitor_templates(db: Session) -> None:
|
||||
"""Seed once when monitor-template table is empty."""
|
||||
"""Seed once when monitor-template table is empty — one template on the full status compare."""
|
||||
if db.query(BizMonitorTemplate.id).limit(1).first():
|
||||
return
|
||||
cmp_svc.ensure_default_templates(db)
|
||||
port_tpl = ensure_port_compare_template(db)
|
||||
zte = (
|
||||
db.query(BizCompareTemplate)
|
||||
.filter(BizCompareTemplate.name == "ZTE status default")
|
||||
.one_or_none()
|
||||
)
|
||||
seeds = [
|
||||
if not zte:
|
||||
return
|
||||
zte_sheets = cmp_svc.template_metrics(zte)
|
||||
zte_overrides = [
|
||||
preset_override_for_metric(
|
||||
str(s.get("metric_id") or ""),
|
||||
sheet_id=str(s.get("sheet_id") or s.get("metric_id") or ""),
|
||||
)
|
||||
for s in zte_sheets
|
||||
if s.get("metric_id")
|
||||
]
|
||||
db.add(
|
||||
BizMonitorTemplate(
|
||||
id=uuid4().hex,
|
||||
name="端口割接监控",
|
||||
compare_template_id=port_tpl.id,
|
||||
collect_metric_ids_json=[PORT_METRIC_ID],
|
||||
name=DEFAULT_MONITOR_TEMPLATE_NAME,
|
||||
compare_template_id=zte.id,
|
||||
collect_metric_ids_json=[],
|
||||
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||
sheet_overrides_json=[preset_override_for_metric(PORT_METRIC_ID)],
|
||||
note="Default: port status dual-verdict with up/down semantics",
|
||||
sheet_overrides_json=zte_overrides,
|
||||
note="Default: full status dual-verdict (port/ARP/BGP/…)",
|
||||
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 ""),
|
||||
sheet_id=str(s.get("sheet_id") or s.get("metric_id") or ""),
|
||||
)
|
||||
for s in zte_sheets
|
||||
if s.get("metric_id")
|
||||
]
|
||||
seeds.append(
|
||||
BizMonitorTemplate(
|
||||
id=uuid4().hex,
|
||||
name="ZTE 状态割接监控",
|
||||
compare_template_id=zte.id,
|
||||
collect_metric_ids_json=[],
|
||||
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||
sheet_overrides_json=zte_overrides,
|
||||
note="ZTE multi-sheet: port/ARP/BGP/… dual presets",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
)
|
||||
for s in seeds:
|
||||
db.add(s)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def default_port_monitor_template_id(db: Session) -> str:
|
||||
"""Ensure seeds exist and return id of 「端口割接监控」 (or first template)."""
|
||||
"""Ensure seeds exist and return the default monitor template id."""
|
||||
ensure_default_monitor_templates(db)
|
||||
row = (
|
||||
db.query(BizMonitorTemplate)
|
||||
.filter(BizMonitorTemplate.name == "端口割接监控")
|
||||
.one_or_none()
|
||||
)
|
||||
if row:
|
||||
return row.id
|
||||
for name in (
|
||||
DEFAULT_MONITOR_TEMPLATE_NAME,
|
||||
"ZTE 状态割接监控",
|
||||
"端口割接监控",
|
||||
):
|
||||
row = (
|
||||
db.query(BizMonitorTemplate)
|
||||
.filter(BizMonitorTemplate.name == name)
|
||||
.one_or_none()
|
||||
)
|
||||
if row:
|
||||
return row.id
|
||||
first = db.query(BizMonitorTemplate).order_by(BizMonitorTemplate.created_at.asc()).first()
|
||||
return first.id if first else ""
|
||||
|
||||
|
|
@ -339,7 +353,7 @@ def list_monitor_templates(db: Session) -> list[dict[str, Any]]:
|
|||
ensure_default_monitor_templates(db)
|
||||
names = _compare_name_map(db)
|
||||
rows = db.query(BizMonitorTemplate).order_by(BizMonitorTemplate.name.asc()).all()
|
||||
return [_out(r, names.get(r.compare_template_id or "", "")) for r in rows]
|
||||
return [_out(r, names.get(r.compare_template_id or "", ""), db=db) for r in rows]
|
||||
|
||||
|
||||
def get_monitor_template(db: Session, template_id: str) -> dict[str, Any]:
|
||||
|
|
@ -347,7 +361,7 @@ def get_monitor_template(db: Session, template_id: str) -> dict[str, Any]:
|
|||
if not row:
|
||||
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||
names = _compare_name_map(db)
|
||||
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||
return _out(row, names.get(row.compare_template_id or "", ""), db=db)
|
||||
|
||||
|
||||
def create_monitor_template(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
@ -381,7 +395,7 @@ def create_monitor_template(db: Session, body: dict[str, Any]) -> dict[str, Any]
|
|||
db.commit()
|
||||
db.refresh(row)
|
||||
names = _compare_name_map(db)
|
||||
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||
return _out(row, names.get(row.compare_template_id or "", ""), db=db)
|
||||
|
||||
|
||||
def update_monitor_template(db: Session, template_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
@ -417,7 +431,7 @@ def update_monitor_template(db: Session, template_id: str, body: dict[str, Any])
|
|||
db.commit()
|
||||
db.refresh(row)
|
||||
names = _compare_name_map(db)
|
||||
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||
return _out(row, names.get(row.compare_template_id or "", ""), db=db)
|
||||
|
||||
|
||||
def delete_monitor_template(db: Session, template_id: str) -> None:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue