Files
ipam_control/app/api/v1/overview.py
T
ayurishchevandClaude Sonnet 5 a846d30872 IPAM Manager: API, UI-админка, журнал аудита
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>
2026-09-20 12:27:47 +03:00

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],
)