mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Move port traffic to device-centric monitoring with a dedicated wall and collect logs.
Split the ops wall to its own route with fullscreen charts, persist collect errors as device events, and polish wall legend layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5079343b2a
commit
214bc56774
24 changed files with 3309 additions and 1357 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""HTTP API for port traffic monitoring."""
|
||||
"""HTTP API for port traffic monitoring (device-centric)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -9,28 +9,30 @@ from sqlalchemy.orm import Session
|
|||
|
||||
from .auth_service import write_audit
|
||||
from .db import get_db
|
||||
from .models import PortTrafficDevice
|
||||
from .port_traffic_schemas import (
|
||||
DiscoverPortsRequest,
|
||||
PortTrafficDeviceCreate,
|
||||
PortTrafficDeviceUpdate,
|
||||
PortTrafficInterfacesPut,
|
||||
PortTrafficReplacePortRequest,
|
||||
PortTrafficTaskCreate,
|
||||
PortTrafficTaskUpdate,
|
||||
PortTrafficTargetsPut,
|
||||
)
|
||||
from .port_traffic_service import (
|
||||
compare_targets,
|
||||
create_task,
|
||||
create_device,
|
||||
dashboard,
|
||||
delete_task,
|
||||
delete_device,
|
||||
discover_ports,
|
||||
get_device,
|
||||
get_samples,
|
||||
get_task,
|
||||
list_device_events,
|
||||
list_devices,
|
||||
list_series,
|
||||
list_targets,
|
||||
list_tasks,
|
||||
put_targets,
|
||||
put_interfaces,
|
||||
replace_series_port,
|
||||
set_task_status,
|
||||
update_task,
|
||||
set_device_status,
|
||||
update_device,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/v1/port-traffic", tags=["port-traffic"])
|
||||
|
|
@ -48,23 +50,23 @@ def api_dashboard(db: Session = Depends(get_db)):
|
|||
return dashboard(db).model_dump()
|
||||
|
||||
|
||||
@router.get("/tasks")
|
||||
def api_list_tasks(
|
||||
@router.get("/devices")
|
||||
def api_list_devices(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return list_tasks(db, page=page, page_size=page_size)
|
||||
return list_devices(db, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.post("/tasks")
|
||||
def api_create_task(
|
||||
body: PortTrafficTaskCreate,
|
||||
@router.post("/devices")
|
||||
def api_create_device(
|
||||
body: PortTrafficDeviceCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
out = create_task(db, body)
|
||||
out = create_device(db, body)
|
||||
if body.start_now and out.status == "running":
|
||||
from .port_traffic_runner import dispatch_collect
|
||||
|
||||
|
|
@ -72,179 +74,206 @@ def api_create_task(
|
|||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.create",
|
||||
action="port_traffic.device.create",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path="/v1/port-traffic/tasks",
|
||||
path="/v1/port-traffic/devices",
|
||||
status_code=200,
|
||||
detail={"id": out.id, "title": out.title, "start_now": body.start_now},
|
||||
detail={"id": out.id, "ne_id": out.ne_id, "start_now": body.start_now},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}")
|
||||
def api_get_task(task_id: str, db: Session = Depends(get_db)):
|
||||
return get_task(db, task_id).model_dump()
|
||||
@router.get("/devices/{device_id}")
|
||||
def api_get_device(device_id: str, db: Session = Depends(get_db)):
|
||||
return get_device(db, device_id).model_dump()
|
||||
|
||||
|
||||
@router.patch("/tasks/{task_id}")
|
||||
def api_patch_task(
|
||||
task_id: str,
|
||||
body: PortTrafficTaskUpdate,
|
||||
@router.patch("/devices/{device_id}")
|
||||
def api_patch_device(
|
||||
device_id: str,
|
||||
body: PortTrafficDeviceUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
out = update_task(db, task_id, body)
|
||||
out = update_device(db, device_id, body)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.update",
|
||||
action="port_traffic.device.update",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="PATCH",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}",
|
||||
path=f"/v1/port-traffic/devices/{device_id}",
|
||||
status_code=200,
|
||||
detail=body.model_dump(exclude_unset=True),
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}")
|
||||
def api_delete_task(task_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = delete_task(db, task_id)
|
||||
@router.delete("/devices/{device_id}")
|
||||
def api_delete_device(device_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = delete_device(db, device_id)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.delete",
|
||||
action="port_traffic.device.delete",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="DELETE",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}",
|
||||
path=f"/v1/port-traffic/devices/{device_id}",
|
||||
status_code=200,
|
||||
detail={"id": task_id},
|
||||
detail={"id": device_id},
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/start")
|
||||
def api_start_task(
|
||||
task_id: str,
|
||||
@router.post("/devices/{device_id}/start")
|
||||
def api_start_device(
|
||||
device_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
out = set_task_status(db, task_id, "running")
|
||||
out = set_device_status(db, device_id, "running")
|
||||
from .port_traffic_runner import dispatch_collect
|
||||
|
||||
background_tasks.add_task(dispatch_collect, task_id)
|
||||
background_tasks.add_task(dispatch_collect, device_id)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.start",
|
||||
action="port_traffic.device.start",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/start",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/start",
|
||||
status_code=200,
|
||||
detail={"id": task_id},
|
||||
detail={"id": device_id},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/collect-now")
|
||||
@router.post("/devices/{device_id}/collect-now")
|
||||
def api_collect_now(
|
||||
task_id: str,
|
||||
device_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
task = get_task(db, task_id)
|
||||
if task.status not in ("running", "paused", "draft", "stopped"):
|
||||
raise HTTPException(status_code=400, detail="invalid_status")
|
||||
# Force due: clear last end so claim accepts, ensure running for this round.
|
||||
from .models import PortTrafficTask
|
||||
device = get_device(db, device_id)
|
||||
from .port_traffic_runner import dispatch_collect
|
||||
|
||||
row = db.get(PortTrafficTask, task_id)
|
||||
row = db.get(PortTrafficDevice, device_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="task_not_found")
|
||||
raise HTTPException(status_code=404, detail="device_not_found")
|
||||
if bool(row.collect_running):
|
||||
return {"ok": True, "started": False, "reason": "already_collecting", **task.model_dump()}
|
||||
return {"ok": True, "started": False, "reason": "already_collecting", **device.model_dump()}
|
||||
if str(row.status) != "running":
|
||||
row.status = "running"
|
||||
row.last_collect_ended_at = None
|
||||
row.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
background_tasks.add_task(dispatch_collect, task_id)
|
||||
background_tasks.add_task(dispatch_collect, device_id)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.collect_now",
|
||||
action="port_traffic.device.collect_now",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/collect-now",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/collect-now",
|
||||
status_code=200,
|
||||
detail={"id": task_id},
|
||||
detail={"id": device_id},
|
||||
)
|
||||
out = get_task(db, task_id)
|
||||
out = get_device(db, device_id)
|
||||
return {"ok": True, "started": True, **out.model_dump()}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/pause")
|
||||
def api_pause_task(task_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = set_task_status(db, task_id, "paused")
|
||||
@router.post("/devices/{device_id}/pause")
|
||||
def api_pause_device(device_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = set_device_status(db, device_id, "paused")
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.pause",
|
||||
action="port_traffic.device.pause",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/pause",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/pause",
|
||||
status_code=200,
|
||||
detail={"id": task_id},
|
||||
detail={"id": device_id},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/stop")
|
||||
def api_stop_task(task_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = set_task_status(db, task_id, "stopped")
|
||||
@router.post("/devices/{device_id}/stop")
|
||||
def api_stop_device(device_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
out = set_device_status(db, device_id, "stopped")
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.task.stop",
|
||||
action="port_traffic.device.stop",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/stop",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/stop",
|
||||
status_code=200,
|
||||
detail={"id": task_id},
|
||||
detail={"id": device_id},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/targets")
|
||||
def api_list_targets(task_id: str, db: Session = Depends(get_db)):
|
||||
return {"items": [t.model_dump() for t in list_targets(db, task_id)]}
|
||||
@router.get("/devices/{device_id}/targets")
|
||||
def api_list_targets(device_id: str, db: Session = Depends(get_db)):
|
||||
return {"items": [t.model_dump() for t in list_targets(db, device_id)]}
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/series")
|
||||
def api_list_series(task_id: str, db: Session = Depends(get_db)):
|
||||
return {"items": [s.model_dump() for s in list_series(db, task_id)]}
|
||||
@router.get("/devices/{device_id}/events")
|
||||
def api_list_device_events(
|
||||
device_id: str,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return list_device_events(db, device_id, limit=limit).model_dump()
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/series/{series_id}/replace")
|
||||
@router.put("/devices/{device_id}/interfaces")
|
||||
def api_put_interfaces(
|
||||
device_id: str,
|
||||
body: PortTrafficInterfacesPut,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
items = put_interfaces(db, device_id, body)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.device.interfaces_put",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="PUT",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/interfaces",
|
||||
status_code=200,
|
||||
detail={"id": device_id, "count": len(items)},
|
||||
)
|
||||
return {"items": [t.model_dump() for t in items]}
|
||||
|
||||
|
||||
@router.get("/devices/{device_id}/series")
|
||||
def api_list_series(device_id: str, db: Session = Depends(get_db)):
|
||||
return {"items": [s.model_dump() for s in list_series(db, device_id)]}
|
||||
|
||||
|
||||
@router.post("/devices/{device_id}/series/{series_id}/replace")
|
||||
def api_replace_series_port(
|
||||
task_id: str,
|
||||
device_id: str,
|
||||
series_id: str,
|
||||
body: PortTrafficReplacePortRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
out = replace_series_port(db, task_id, series_id, body)
|
||||
out = replace_series_port(db, device_id, series_id, body)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
|
|
@ -252,35 +281,13 @@ def api_replace_series_port(
|
|||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="POST",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/series/{series_id}/replace",
|
||||
path=f"/v1/port-traffic/devices/{device_id}/series/{series_id}/replace",
|
||||
status_code=200,
|
||||
detail={"series_id": series_id, "ifname": body.ifname, "target_id": body.target_id},
|
||||
detail={"series_id": series_id, "ifname": body.ifname},
|
||||
)
|
||||
return out.model_dump()
|
||||
|
||||
|
||||
@router.put("/tasks/{task_id}/targets")
|
||||
def api_put_targets(
|
||||
task_id: str,
|
||||
body: PortTrafficTargetsPut,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
items = put_targets(db, task_id, body)
|
||||
uid, uname = _actor(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="port_traffic.targets.put",
|
||||
actor_user_id=uid,
|
||||
actor_username=uname,
|
||||
method="PUT",
|
||||
path=f"/v1/port-traffic/tasks/{task_id}/targets",
|
||||
status_code=200,
|
||||
detail={"id": task_id, "count": len(items)},
|
||||
)
|
||||
return {"items": [t.model_dump() for t in items]}
|
||||
|
||||
|
||||
@router.post("/discover/ports")
|
||||
def api_discover_ports(body: DiscoverPortsRequest, db: Session = Depends(get_db)):
|
||||
return discover_ports(db, body).model_dump()
|
||||
|
|
@ -304,7 +311,7 @@ def api_compare(
|
|||
offset_hours: float | None = Query(default=None, ge=0.25, le=24 * 90),
|
||||
baseline_target_id: str | None = Query(
|
||||
default=None,
|
||||
description="optional mapped interface for baseline overlay (any task)",
|
||||
description="optional mapped interface for baseline overlay",
|
||||
),
|
||||
to_ts: datetime | None = Query(default=None, alias="to"),
|
||||
db: Session = Depends(get_db),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue