249 lines
8.5 KiB
Python
249 lines
8.5 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_current_user, get_db
|
|
from app.models.device import Device
|
|
from app.models.object import Object
|
|
from app.models.snapshot import ConfigSnapshot
|
|
from app.models.snapshot_policy import SnapshotPolicy
|
|
from app.models.user import User
|
|
from app.schemas.snapshot_policy import (
|
|
ObjectSnapshotSummary,
|
|
PolicySnapshotStatus,
|
|
SnapshotPolicyCreate,
|
|
SnapshotPolicyRead,
|
|
SnapshotPolicyUpdate,
|
|
)
|
|
from app.services import task as task_service
|
|
|
|
router = APIRouter(prefix="/api/v1", tags=["snapshot-policies"])
|
|
|
|
|
|
def _policy_snapshot_label(policy: SnapshotPolicy) -> str:
|
|
if policy.action_type == "mikrotik_get_config":
|
|
return "routeros-export"
|
|
return policy.params.get("path", "")
|
|
|
|
|
|
# ─── Dashboard ───────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/snapshots/dashboard", response_model=list[ObjectSnapshotSummary])
|
|
async def snapshot_dashboard(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(get_current_user),
|
|
):
|
|
# 1. Load all active global policies
|
|
policies_result = await db.execute(
|
|
select(SnapshotPolicy).where(SnapshotPolicy.is_active == True).order_by(SnapshotPolicy.label)
|
|
)
|
|
policies = policies_result.scalars().all()
|
|
if not policies:
|
|
return []
|
|
|
|
# Collect categories we care about
|
|
categories = list({p.category for p in policies})
|
|
|
|
# 2. Find all active objects that have devices in relevant categories
|
|
obj_q = (
|
|
select(Object.id, Object.name, Object.city, Object.client)
|
|
.where(Object.is_active == True)
|
|
.order_by(Object.city, Object.name)
|
|
)
|
|
obj_result = await db.execute(obj_q)
|
|
all_objects = obj_result.all()
|
|
|
|
if not all_objects:
|
|
return []
|
|
|
|
# 3. Count eligible devices per (object_id, category)
|
|
eligible_q = (
|
|
select(
|
|
Device.object_id,
|
|
Device.category,
|
|
func.count(Device.id).label("total"),
|
|
)
|
|
.where(Device.is_active == True, Device.category.in_(categories))
|
|
.group_by(Device.object_id, Device.category)
|
|
)
|
|
eligible_result = await db.execute(eligible_q)
|
|
eligible_map: dict[tuple[int, str], int] = {
|
|
(r.object_id, r.category): r.total for r in eligible_result.all()
|
|
}
|
|
|
|
# 4. Snapshot stats per (object_id, category, label)
|
|
stats_q = (
|
|
select(
|
|
Device.object_id,
|
|
Device.category,
|
|
ConfigSnapshot.label,
|
|
func.count(func.distinct(ConfigSnapshot.device_id)).label("success_count"),
|
|
func.max(ConfigSnapshot.created_at).label("last_collected_at"),
|
|
)
|
|
.join(Device, ConfigSnapshot.device_id == Device.id)
|
|
.where(Device.is_active == True, Device.category.in_(categories))
|
|
.group_by(Device.object_id, Device.category, ConfigSnapshot.label)
|
|
)
|
|
stats_result = await db.execute(stats_q)
|
|
stats_map: dict[tuple[int, str, str], tuple[int, object]] = {
|
|
(r.object_id, r.category, r.label): (r.success_count, r.last_collected_at)
|
|
for r in stats_result.all()
|
|
}
|
|
|
|
# 5. Assemble: for each object, build policy statuses
|
|
result = []
|
|
for obj_id, obj_name, obj_city, obj_client in all_objects:
|
|
obj_policies = []
|
|
for policy in policies:
|
|
total = eligible_map.get((obj_id, policy.category), 0)
|
|
if total == 0:
|
|
continue
|
|
snap_label = _policy_snapshot_label(policy)
|
|
stats = stats_map.get((obj_id, policy.category, snap_label))
|
|
success_count = stats[0] if stats else 0
|
|
last_collected_at = stats[1] if stats else None
|
|
|
|
obj_policies.append(PolicySnapshotStatus(
|
|
policy_id=policy.id,
|
|
policy_label=policy.label,
|
|
category=policy.category,
|
|
action_type=policy.action_type,
|
|
params=policy.params,
|
|
last_collected_at=last_collected_at,
|
|
success_count=success_count,
|
|
total_devices=total,
|
|
))
|
|
|
|
if obj_policies:
|
|
result.append(ObjectSnapshotSummary(
|
|
object_id=obj_id,
|
|
object_name=obj_name,
|
|
city=obj_city,
|
|
client=obj_client,
|
|
policies=obj_policies,
|
|
))
|
|
|
|
return result
|
|
|
|
|
|
# ─── Policies CRUD (global) ──────────────────────────────────────────────────
|
|
|
|
@router.get("/snapshot-policies", response_model=list[SnapshotPolicyRead])
|
|
async def list_policies(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(get_current_user),
|
|
):
|
|
result = await db.execute(
|
|
select(SnapshotPolicy).order_by(SnapshotPolicy.label)
|
|
)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.post("/snapshot-policies", response_model=SnapshotPolicyRead, status_code=201)
|
|
async def create_policy(
|
|
body: SnapshotPolicyCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(get_current_user),
|
|
):
|
|
policy = SnapshotPolicy(
|
|
category=body.category,
|
|
action_type=body.action_type,
|
|
params=body.params,
|
|
label=body.label,
|
|
is_active=body.is_active,
|
|
)
|
|
db.add(policy)
|
|
await db.flush()
|
|
await db.refresh(policy)
|
|
return policy
|
|
|
|
|
|
@router.put("/snapshot-policies/{policy_id}", response_model=SnapshotPolicyRead)
|
|
async def update_policy(
|
|
policy_id: int,
|
|
body: SnapshotPolicyUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(get_current_user),
|
|
):
|
|
policy = await db.get(SnapshotPolicy, policy_id)
|
|
if policy is None:
|
|
raise HTTPException(status_code=404, detail="Policy not found")
|
|
|
|
for field, value in body.model_dump(exclude_unset=True).items():
|
|
setattr(policy, field, value)
|
|
|
|
await db.flush()
|
|
await db.refresh(policy)
|
|
return policy
|
|
|
|
|
|
@router.delete("/snapshot-policies/{policy_id}", status_code=204)
|
|
async def delete_policy(
|
|
policy_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(get_current_user),
|
|
):
|
|
policy = await db.get(SnapshotPolicy, policy_id)
|
|
if policy is None:
|
|
raise HTTPException(status_code=404, detail="Policy not found")
|
|
|
|
await db.delete(policy)
|
|
await db.flush()
|
|
|
|
|
|
# ─── Collect ─────────────────────────────────────────────────────────────────
|
|
|
|
@router.post("/snapshot-policies/{policy_id}/collect", status_code=202)
|
|
async def collect_all(
|
|
policy_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Collect snapshots for ALL objects matching this policy's category."""
|
|
policy = await db.get(SnapshotPolicy, policy_id)
|
|
if policy is None:
|
|
raise HTTPException(status_code=404, detail="Policy not found")
|
|
if not policy.is_active:
|
|
raise HTTPException(status_code=400, detail="Policy is disabled")
|
|
|
|
task = await task_service.create_task(
|
|
action_type=policy.action_type,
|
|
target_scope={
|
|
"type": "category",
|
|
"category": policy.category,
|
|
},
|
|
params=policy.params,
|
|
user_id=current_user.id,
|
|
)
|
|
return {"task_id": task.id}
|
|
|
|
|
|
@router.post("/snapshot-policies/{policy_id}/collect/{obj_id}", status_code=202)
|
|
async def collect_for_object(
|
|
policy_id: int,
|
|
obj_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Collect snapshots for a specific object matching this policy's category."""
|
|
policy = await db.get(SnapshotPolicy, policy_id)
|
|
if policy is None:
|
|
raise HTTPException(status_code=404, detail="Policy not found")
|
|
if not policy.is_active:
|
|
raise HTTPException(status_code=400, detail="Policy is disabled")
|
|
|
|
obj = await db.get(Object, obj_id)
|
|
if obj is None:
|
|
raise HTTPException(status_code=404, detail="Object not found")
|
|
|
|
task = await task_service.create_task(
|
|
action_type=policy.action_type,
|
|
target_scope={
|
|
"type": "category",
|
|
"object_id": obj_id,
|
|
"category": policy.category,
|
|
},
|
|
params=policy.params,
|
|
user_id=current_user.id,
|
|
)
|
|
return {"task_id": task.id}
|