netx/netx_api/biz_state_router.py
2026-09-17 18:57:25 +08:00

371 lines
11 KiB
Python

"""HTTP API for business state monitoring (/v1/biz-state)."""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, BackgroundTasks, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from .db import get_db
from .biz_state import service as svc
from .biz_state.collect_runner import dispatch_collect
from .lldp_shared import resolve_vendor_key
from .models import BizStateTask
router = APIRouter(prefix="/v1/biz-state", tags=["biz-state"])
class ProfileOverrideIn(BaseModel):
title: str | None = None
command_template: str | None = None
description: str | None = None
sample_output: str | None = None
enabled: bool | None = None
class TaskItemIn(BaseModel):
source_profile_id: str = ""
kind: str = "catalog"
enabled: bool = True
title: str = ""
command_override: str = ""
sort_order: int | None = None
bindings: list[dict[str, str]] = Field(default_factory=list)
class TaskCreateIn(BaseModel):
source: str = "managed"
ne_id: str
ne_name: str = ""
ne_ip: str = ""
vendor: str = ""
device_type: str = ""
note: str = ""
interval_sec: int = 3600
retention_batches: int = 30
items: list[TaskItemIn] = Field(default_factory=list)
class TaskPatchIn(BaseModel):
note: str | None = None
interval_sec: int | None = None
retention_batches: int | None = None
status: str | None = None
items: list[TaskItemIn] | None = None
class PreviewIn(BaseModel):
vendor: str = ""
device_type: str = ""
items: list[TaskItemIn] = Field(default_factory=list)
class DiscoverIn(BaseModel):
source: str = "managed"
ne_id: str = ""
task_id: str = ""
discover_profile_id: str = ""
collect_profile_id: str = ""
placeholder: str = ""
class BindingsIn(BaseModel):
bindings: list[dict[str, str]] = Field(default_factory=list)
@router.get("/profiles")
def api_list_profiles(
vendor: str = "",
device_type: str = "",
kind: str = "",
db: Session = Depends(get_db),
) -> dict[str, Any]:
vkey = resolve_vendor_key(vendor, device_type) if (vendor or device_type) else ""
items = svc.list_profiles_public(db, vendor_key=vkey)
if kind:
items = [p for p in items if str(p.get("kind") or "") == kind]
return {"items": items}
@router.patch("/profiles/{profile_id}")
def api_patch_profile(
profile_id: str,
body: ProfileOverrideIn,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return svc.upsert_profile_override(db, profile_id, body.model_dump(exclude_unset=True))
@router.post("/tasks/preview-items")
def api_preview_items(
body: PreviewIn,
db: Session = Depends(get_db),
) -> dict[str, Any]:
items = [i.model_dump() for i in body.items]
return {
"items": svc.preview_items(
db, vendor=body.vendor, device_type=body.device_type, items=items
)
}
@router.post("/discover")
def api_discover(body: DiscoverIn, db: Session = Depends(get_db)) -> dict[str, Any]:
from .biz_state.discover import discover_params
return discover_params(
db,
source=body.source,
ne_id=body.ne_id,
task_id=body.task_id,
discover_profile_id=body.discover_profile_id,
collect_profile_id=body.collect_profile_id,
placeholder=body.placeholder,
)
@router.put("/tasks/{task_id}/items/{item_id}/bindings")
def api_set_bindings(
task_id: str,
item_id: str,
body: BindingsIn,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return svc.set_item_bindings(db, task_id, item_id, list(body.bindings or []))
@router.get("/tasks")
def api_list_tasks(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": svc.list_tasks(db)}
@router.post("/tasks")
def api_create_task(body: TaskCreateIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return svc.create_task(db, body.model_dump())
@router.get("/tasks/{task_id}")
def api_get_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return svc.get_task(db, task_id)
@router.patch("/tasks/{task_id}")
def api_patch_task(
task_id: str, body: TaskPatchIn, db: Session = Depends(get_db)
) -> dict[str, Any]:
return svc.update_task(db, task_id, body.model_dump(exclude_unset=True))
@router.delete("/tasks/{task_id}")
def api_delete_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
svc.delete_task(db, task_id)
return {"ok": True}
@router.post("/tasks/{task_id}/collect")
def api_collect_now(
task_id: str,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
) -> dict[str, Any]:
task = db.get(BizStateTask, task_id)
if not task:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="task not found")
if bool(task.collect_running):
return {"ok": True, "started": False, "reason": "already_collecting", "task_id": task_id}
# Allow one-shot from draft/paused
task.last_collect_ended_at = None
db.commit()
background_tasks.add_task(dispatch_collect, task_id)
return {"ok": True, "started": True, "task_id": task_id}
@router.get("/tasks/{task_id}/batches")
def api_list_batches(
task_id: str, limit: int = 50, db: Session = Depends(get_db)
) -> dict[str, Any]:
return {"items": svc.list_batches(db, task_id, limit=limit)}
@router.get("/batches/{batch_id}")
def api_get_batch(batch_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return svc.get_batch(db, batch_id)
@router.get("/batches/{batch_id}/export")
def api_export_batch(batch_id: str, db: Session = Depends(get_db)) -> StreamingResponse:
data = svc.export_batch_zip(db, batch_id)
return StreamingResponse(
iter([data]),
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="biz_state_{batch_id}.zip"'},
)
# ---- Phase2: templates / port maps / compare jobs ----
from .biz_state import compare_service as cmp_svc # noqa: E402
class TemplateMetricIn(BaseModel):
metric_id: str
key_fields: list[str] = Field(default_factory=list)
iface_fields: list[str] = Field(default_factory=list)
compare_fields: list[str] = Field(default_factory=list)
class TemplateIn(BaseModel):
name: str = ""
note: str = ""
# Preferred: multi-metric sheets
metrics: list[TemplateMetricIn] | None = None
# Legacy single-metric fields (still accepted)
metric_id: str = "lldp_neighbor"
key_fields: list[str] = Field(default_factory=list)
iface_fields: list[str] = Field(default_factory=list)
compare_fields: list[str] = Field(default_factory=list)
ignore_fields: list[str] = Field(default_factory=list)
class TemplatePatchIn(BaseModel):
name: str | None = None
note: str | None = None
metrics: list[TemplateMetricIn] | None = None
metric_id: str | None = None
key_fields: list[str] | None = None
iface_fields: list[str] | None = None
compare_fields: list[str] | None = None
ignore_fields: list[str] | None = None
class MappingRowIn(BaseModel):
before_if: str
after_if: str
class MappingIn(BaseModel):
name: str = ""
note: str = ""
rows: list[MappingRowIn] = Field(default_factory=list)
class ValidateMappingIn(BaseModel):
mapping_id: str
before_batch_id: str
after_batch_id: str
template_id: str = ""
class CompareJobIn(BaseModel):
name: str = ""
template_id: str = ""
mapping_id: str = ""
before_task_id: str = ""
after_task_id: str = ""
before_batch_id: str = ""
after_batch_id: str = ""
mode: str = "manual"
note: str = ""
@router.get("/compare/metrics")
def api_list_compare_metrics() -> dict[str, Any]:
return {"items": cmp_svc.list_metric_schemas()}
@router.get("/compare/templates")
def api_list_templates(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_templates(db)}
@router.post("/compare/templates")
def api_create_template(body: TemplateIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.create_template(db, body.model_dump())
@router.patch("/compare/templates/{template_id}")
def api_patch_template(
template_id: str, body: TemplatePatchIn, db: Session = Depends(get_db)
) -> dict[str, Any]:
return cmp_svc.update_template(db, template_id, body.model_dump(exclude_unset=True))
@router.delete("/compare/templates/{template_id}")
def api_delete_template(template_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
cmp_svc.delete_template(db, template_id)
return {"ok": True}
@router.get("/compare/mappings")
def api_list_mappings(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_mappings(db)}
@router.post("/compare/mappings")
def api_create_mapping(body: MappingIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.create_mapping(db, body.model_dump())
@router.patch("/compare/mappings/{mapping_id}")
def api_patch_mapping(
mapping_id: str, body: MappingIn, db: Session = Depends(get_db)
) -> dict[str, Any]:
return cmp_svc.update_mapping(db, mapping_id, body.model_dump(exclude_unset=True))
@router.delete("/compare/mappings/{mapping_id}")
def api_delete_mapping(mapping_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
cmp_svc.delete_mapping(db, mapping_id)
return {"ok": True}
@router.post("/compare/mappings/validate")
def api_validate_mapping(body: ValidateMappingIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.validate_mapping(
db,
mapping_id=body.mapping_id,
before_batch_id=body.before_batch_id,
after_batch_id=body.after_batch_id,
template_id=body.template_id,
)
@router.get("/compare/jobs")
def api_list_jobs(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_jobs(db)}
@router.post("/compare/jobs")
def api_create_job(body: CompareJobIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.create_job(db, body.model_dump())
@router.patch("/compare/jobs/{job_id}")
def api_patch_job(job_id: str, body: CompareJobIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.update_job(db, job_id, body.model_dump(exclude_unset=True))
@router.delete("/compare/jobs/{job_id}")
def api_delete_job(job_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
cmp_svc.delete_job(db, job_id)
return {"ok": True}
@router.post("/compare/jobs/{job_id}/run")
def api_run_job(job_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.run_compare(db, job_id)
@router.get("/compare/jobs/{job_id}/runs")
def api_list_runs(job_id: str, limit: int = 20, db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_runs(db, job_id, limit=limit)}
@router.get("/compare/runs/{run_id}")
def api_get_run(run_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.get_run(db, run_id)