Backend (FastAPI, SQLAlchemy 2, Alembic, PostgreSQL 16): - организации, VRF, префиксы (дерево, использование, автоназначение), адреса, операторы связи, устройства и типы устройств; JWT, роли admin/viewer; - VRF принадлежит организации (составной FK), смена VRF у префикса переносит поддерево, имя VRF уникально в организации; - журнал аудита: поиск и фильтры, ротация (срок/количество), очистка по паролю с блокировкой, IP клиента и метаданные запроса (X-Forwarded-For только от TRUSTED_PROXIES). UI (web/, без сборки): экраны и диалоги по макетам «IPAM Manager», кликабельные строки реестров, локальные шрифты IBM Plex, собственные выпадающие списки. Окружение: docker-compose (postgres + app), миграции Alembic 0001-0004, scripts/gen_env.py, scripts/seed_demo.py, 11 автотестов (pytest). Документация: README.md и docs/changes/001-005 (планы и итоги). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
46 lines
2.2 KiB
Python
46 lines
2.2 KiB
Python
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app import schemas as s
|
|
from app.api.v1.prefixes import _prefix_outs
|
|
from app.db import get_db
|
|
from app.models import Address, AddressStatus, AuditLog, Prefix, PrefixStatus, Vrf
|
|
from app.security import current_user
|
|
from app.services import capacity, utilization
|
|
|
|
router = APIRouter(dependencies=[Depends(current_user)])
|
|
|
|
|
|
class Overview(BaseModel):
|
|
prefixes: int
|
|
vrfs: int
|
|
assigned: int
|
|
capacity: int
|
|
utilization: int
|
|
reserved: int
|
|
top_prefixes: list[s.PrefixOut]
|
|
recent_changes: list[s.AuditOut]
|
|
|
|
|
|
@router.get("/overview", response_model=Overview, tags=["overview"])
|
|
def overview(db: Session = Depends(get_db)):
|
|
prefixes = db.scalars(select(Prefix).where(Prefix.status == PrefixStatus.active)).all()
|
|
outs = _prefix_outs(db, list(prefixes))
|
|
parents = {p.parent_id for p in outs if p.parent_id}
|
|
leaves = [p for p in outs if p.id not in parents] # ёмкость считаем по листьям, чтобы не удваивать
|
|
leaves4 = [p for p in leaves if p.family == 4] # IPv6-пространство несопоставимо по размеру — в метрики использования не входит
|
|
cap = sum(p.capacity for p in leaves4)
|
|
v4 = func.family(Address.address) == 4
|
|
assigned = db.scalar(select(func.count()).select_from(Address).where(Address.status == AddressStatus.assigned, v4)) or 0
|
|
reserved = db.scalar(select(func.count()).select_from(Address).where(Address.status == AddressStatus.reserved, v4)) or 0
|
|
top = sorted((p for p in leaves4 if p.capacity > 1), key=lambda p: p.utilization, reverse=True)[:4]
|
|
recent = db.scalars(select(AuditLog).order_by(AuditLog.id.desc()).limit(4)).all()
|
|
return Overview(
|
|
prefixes=db.scalar(select(func.count()).select_from(Prefix)) or 0,
|
|
vrfs=db.scalar(select(func.count()).select_from(Vrf)) or 0,
|
|
assigned=assigned, capacity=cap, utilization=utilization(assigned, cap), reserved=reserved,
|
|
top_prefixes=top, recent_changes=[s.AuditOut.from_row(r) for r in recent],
|
|
)
|