mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
Add cutover-compare nav group and monitor templates.
Move biz monitor/compare/migration under Cutover compare, split compare templates into their own page, and add monitor-template CRUD that overlays compare templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f3a8f80822
commit
202196a197
13 changed files with 812 additions and 36 deletions
216
netx_api/biz_migration/monitor_templates.py
Normal file
216
netx_api/biz_migration/monitor_templates.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
"""CRUD for BizMonitorTemplate (cutover overlay on compare templates)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException
|
||||
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 .evaluate import PORT_METRIC_ID, PORT_STATUS_FIELDS
|
||||
|
||||
|
||||
def _utcnow():
|
||||
return utcnow_naive()
|
||||
|
||||
|
||||
def _out(row: BizMonitorTemplate, compare_name: str = "") -> dict[str, Any]:
|
||||
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 []),
|
||||
"defaults": dict(row.defaults_json or {}),
|
||||
"sheet_overrides": list(row.sheet_overrides_json or []),
|
||||
"note": row.note or "",
|
||||
"created_at": row.created_at.isoformat() + "Z" if row.created_at else None,
|
||||
"updated_at": row.updated_at.isoformat() + "Z" if row.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
def _compare_name_map(db: Session) -> dict[str, str]:
|
||||
rows = db.query(BizCompareTemplate.id, BizCompareTemplate.name).all()
|
||||
return {str(r.id): str(r.name or "") for r in rows}
|
||||
|
||||
|
||||
def ensure_port_compare_template(db: Session) -> BizCompareTemplate:
|
||||
"""Built-in HOW template for interface_brief (matches former port_sheet_def)."""
|
||||
name = "Cutover port status"
|
||||
row = db.query(BizCompareTemplate).filter(BizCompareTemplate.name == name).one_or_none()
|
||||
sheet = {
|
||||
"metric_id": PORT_METRIC_ID,
|
||||
"key_fields": ["interface"],
|
||||
"iface_fields": ["interface"],
|
||||
"compare_fields": list(PORT_STATUS_FIELDS),
|
||||
"display_fields": ["interface", *PORT_STATUS_FIELDS, "description"],
|
||||
"row_filters": [],
|
||||
"field_rules": [],
|
||||
}
|
||||
if row:
|
||||
return row
|
||||
row = BizCompareTemplate(
|
||||
id=uuid4().hex,
|
||||
name=name,
|
||||
note="Built-in port status sheet for cutover monitor (admin/phy/prot)",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
cmp_svc._apply_sheets_to_row(row, [sheet]) # noqa: SLF001 — shared normalizer
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
|
||||
|
||||
def ensure_default_monitor_templates(db: Session) -> None:
|
||||
"""Seed once when monitor-template table is empty."""
|
||||
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 = [
|
||||
BizMonitorTemplate(
|
||||
id=uuid4().hex,
|
||||
name="端口割接监控",
|
||||
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"],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
note="Default: port status dual-verdict with up/down semantics",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
),
|
||||
]
|
||||
if zte:
|
||||
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=[],
|
||||
note="Uses ZTE status compare template; presence dual-verdict",
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
)
|
||||
for s in seeds:
|
||||
db.add(s)
|
||||
db.commit()
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def get_monitor_template(db: Session, template_id: str) -> dict[str, Any]:
|
||||
row = db.get(BizMonitorTemplate, template_id)
|
||||
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 "", ""))
|
||||
|
||||
|
||||
def create_monitor_template(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||
name = str(body.get("name") or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name_required")
|
||||
compare_id = str(body.get("compare_template_id") or "").strip()
|
||||
if compare_id and not db.get(BizCompareTemplate, compare_id):
|
||||
raise HTTPException(status_code=404, detail="compare_template_not_found")
|
||||
collect = body.get("collect_metric_ids")
|
||||
if not isinstance(collect, list):
|
||||
collect = []
|
||||
defaults = body.get("defaults")
|
||||
if not isinstance(defaults, dict):
|
||||
defaults = {"dual_mode": "migrate_pair", "out_of_expect": "strict"}
|
||||
overrides = body.get("sheet_overrides")
|
||||
if not isinstance(overrides, list):
|
||||
overrides = []
|
||||
row = BizMonitorTemplate(
|
||||
id=uuid4().hex,
|
||||
name=name[:256],
|
||||
compare_template_id=compare_id,
|
||||
collect_metric_ids_json=[str(x) for x in collect if str(x).strip()],
|
||||
defaults_json=defaults,
|
||||
sheet_overrides_json=overrides,
|
||||
note=str(body.get("note") or "")[:512],
|
||||
created_at=_utcnow(),
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
names = _compare_name_map(db)
|
||||
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||
|
||||
|
||||
def update_monitor_template(db: Session, template_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
row = db.get(BizMonitorTemplate, template_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||
if "name" in body and body["name"] is not None:
|
||||
name = str(body["name"] or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name_required")
|
||||
row.name = name[:256]
|
||||
if "compare_template_id" in body and body["compare_template_id"] is not None:
|
||||
compare_id = str(body["compare_template_id"] or "").strip()
|
||||
if compare_id and not db.get(BizCompareTemplate, compare_id):
|
||||
raise HTTPException(status_code=404, detail="compare_template_not_found")
|
||||
row.compare_template_id = compare_id
|
||||
if "collect_metric_ids" in body and body["collect_metric_ids"] is not None:
|
||||
collect = body["collect_metric_ids"]
|
||||
if not isinstance(collect, list):
|
||||
raise HTTPException(status_code=400, detail="collect_metric_ids_invalid")
|
||||
row.collect_metric_ids_json = [str(x) for x in collect if str(x).strip()]
|
||||
if "defaults" in body and body["defaults"] is not None:
|
||||
if not isinstance(body["defaults"], dict):
|
||||
raise HTTPException(status_code=400, detail="defaults_invalid")
|
||||
row.defaults_json = body["defaults"]
|
||||
if "sheet_overrides" in body and body["sheet_overrides"] is not None:
|
||||
if not isinstance(body["sheet_overrides"], list):
|
||||
raise HTTPException(status_code=400, detail="sheet_overrides_invalid")
|
||||
row.sheet_overrides_json = body["sheet_overrides"]
|
||||
if "note" in body and body["note"] is not None:
|
||||
row.note = str(body["note"] or "")[:512]
|
||||
row.updated_at = _utcnow()
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
names = _compare_name_map(db)
|
||||
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||
|
||||
|
||||
def delete_monitor_template(db: Session, template_id: str) -> None:
|
||||
row = db.get(BizMonitorTemplate, template_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
|
|
@ -9,6 +9,7 @@ from pydantic import BaseModel, Field
|
|||
from sqlalchemy.orm import Session
|
||||
|
||||
from .biz_migration import service as svc
|
||||
from .biz_migration import monitor_templates as mon_tpl
|
||||
from .db import get_db
|
||||
|
||||
router = APIRouter(prefix="/v1/biz-migration", tags=["biz-migration"])
|
||||
|
|
@ -41,6 +42,24 @@ class BatchIn(BaseModel):
|
|||
note: str = ""
|
||||
|
||||
|
||||
class MonitorTemplateIn(BaseModel):
|
||||
name: str = ""
|
||||
compare_template_id: str = ""
|
||||
collect_metric_ids: list[str] = Field(default_factory=list)
|
||||
defaults: dict[str, Any] = Field(default_factory=dict)
|
||||
sheet_overrides: list[dict[str, Any]] = Field(default_factory=list)
|
||||
note: str = ""
|
||||
|
||||
|
||||
class MonitorTemplatePatchIn(BaseModel):
|
||||
name: str | None = None
|
||||
compare_template_id: str | None = None
|
||||
collect_metric_ids: list[str] | None = None
|
||||
defaults: dict[str, Any] | None = None
|
||||
sheet_overrides: list[dict[str, Any]] | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class BatchPatchIn(BaseModel):
|
||||
batch_label: str | None = None
|
||||
expect_set: dict[str, Any] | None = None
|
||||
|
|
@ -209,3 +228,35 @@ def api_list_diffs(
|
|||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/monitor-templates")
|
||||
def api_list_monitor_templates(db: Session = Depends(get_db)):
|
||||
return {"items": mon_tpl.list_monitor_templates(db)}
|
||||
|
||||
|
||||
@router.post("/monitor-templates")
|
||||
def api_create_monitor_template(body: MonitorTemplateIn, db: Session = Depends(get_db)):
|
||||
return mon_tpl.create_monitor_template(db, body.model_dump())
|
||||
|
||||
|
||||
@router.get("/monitor-templates/{template_id}")
|
||||
def api_get_monitor_template(template_id: str, db: Session = Depends(get_db)):
|
||||
return mon_tpl.get_monitor_template(db, template_id)
|
||||
|
||||
|
||||
@router.patch("/monitor-templates/{template_id}")
|
||||
def api_patch_monitor_template(
|
||||
template_id: str,
|
||||
body: MonitorTemplatePatchIn,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return mon_tpl.update_monitor_template(
|
||||
db, template_id, body.model_dump(exclude_unset=True)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/monitor-templates/{template_id}")
|
||||
def api_delete_monitor_template(template_id: str, db: Session = Depends(get_db)):
|
||||
mon_tpl.delete_monitor_template(db, template_id)
|
||||
return {"ok": True}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,19 @@ def apply_biz_state_schema(conn: Connection) -> None:
|
|||
"ALTER TABLE biz_migration_batch ADD COLUMN IF NOT EXISTS accept_summary_json JSON DEFAULT '{}'",
|
||||
"ALTER TABLE biz_migration_run ADD COLUMN IF NOT EXISTS purpose VARCHAR(32) DEFAULT 'manual'",
|
||||
"CREATE INDEX IF NOT EXISTS ix_biz_migration_red_project_status ON biz_migration_red_ticket (project_id, status)",
|
||||
"CREATE TABLE IF NOT EXISTS biz_monitor_template ("
|
||||
"id VARCHAR(64) PRIMARY KEY,"
|
||||
"name VARCHAR(256) DEFAULT '',"
|
||||
"compare_template_id VARCHAR(64) DEFAULT '',"
|
||||
"collect_metric_ids_json JSON DEFAULT '[]',"
|
||||
"defaults_json JSON DEFAULT '{}',"
|
||||
"sheet_overrides_json JSON DEFAULT '[]',"
|
||||
"note VARCHAR(512) DEFAULT '',"
|
||||
"created_at TIMESTAMP,"
|
||||
"updated_at TIMESTAMP"
|
||||
")",
|
||||
"CREATE INDEX IF NOT EXISTS ix_biz_monitor_template_name ON biz_monitor_template (name)",
|
||||
"CREATE INDEX IF NOT EXISTS ix_biz_monitor_template_compare ON biz_monitor_template (compare_template_id)",
|
||||
):
|
||||
try:
|
||||
_run_sql(conn, sql)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from .biz_state import (
|
|||
BizCompareJob,
|
||||
BizCompareRun,
|
||||
BizCompareTemplate,
|
||||
BizMonitorTemplate,
|
||||
BizPortMapping,
|
||||
BizPortMappingRow,
|
||||
BizStateBatch,
|
||||
|
|
@ -148,6 +149,7 @@ __all__ = [
|
|||
"BizStateEvent",
|
||||
"BizStateCommandOverride",
|
||||
"BizCompareTemplate",
|
||||
"BizMonitorTemplate",
|
||||
"BizPortMapping",
|
||||
"BizPortMappingRow",
|
||||
"BizCompareJob",
|
||||
|
|
|
|||
|
|
@ -221,6 +221,27 @@ class BizCompareTemplate(Base):
|
|||
ignore_fields: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||
metrics_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||
note: Mapped[str] = mapped_column(String(512), default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive, index=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||
|
||||
|
||||
class BizMonitorTemplate(Base):
|
||||
"""Cutover/monitor overlay on a BizCompareTemplate (HOW + WHEN rules).
|
||||
|
||||
``compare_template_id`` selects sheets / field rules.
|
||||
``defaults_json`` / ``sheet_overrides_json`` carry dual-verdict & status semantics.
|
||||
``collect_metric_ids_json`` optional HF collect subset.
|
||||
"""
|
||||
|
||||
__tablename__ = "biz_monitor_template"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
|
||||
name: Mapped[str] = mapped_column(String(256), default="", index=True)
|
||||
compare_template_id: Mapped[str] = mapped_column(String(64), default="", index=True)
|
||||
collect_metric_ids_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||
defaults_json: Mapped[dict] = mapped_column(_JsonType, default=dict)
|
||||
sheet_overrides_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||
note: Mapped[str] = mapped_column(String(512), default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue