Files
ipam_control/app/api/v1/overview.py
T
ayurishchevandClaude Opus 5.5 03d727e496 Задачи 025-030: ёмкость префиксов, политика входа, дерево префиксов
Повторный анализ кодовой базы (docs/reviews/2026-09-26-codebase-review-2.md) и доработки:
025 Ёмкость префикса — размер его подсети (а не сумма листьев); «Обзор» считает ёмкость
    по корневым активным IPv4-префиксам и адреса внутри них.
026 Политика блокировки входа: 5 неудач на логин+IP, 20 на IP, 50 на логин со всех IP,
    кроме известных IP (known_logins, миграция 0009) — владельца нельзя заблокировать анонимно.
027 Сериализация попыток входа по IP (advisory-lock после блокировки логина).
028 UI «Префиксы»: загрузка всех страниц (до 20 000), счётчики по total, предупреждение об усечении.
029 Advisory-lock по VRF для операций, меняющих дерево префиксов и раскладку адресов.
030 Исправление замечаний ревью 025-029: _lock_prefix (VRF блокируется до чтения префикса,
    409 при одновременном переносе), константы политики входа перенесены в app/services.py.

Тесты: 14 passed (проверка ёмкости родителя приведена к семантике 025); сквозные сценарии
и гонки — docs/reviews/2026-09-26-changes-025-029-review.md, 2026-09-27-changes-030-review.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:28:50 +03:00

68 lines
3.3 KiB
Python

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import and_, 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]
def _ipv4_roots(db: Session):
"""Активные IPv4-префиксы, не вложенные ни в один другой активный IPv4-префикс того же VRF (по CIDR, не по
parent_id — дерево может включать неактивные ветки). Основа ёмкости и загрузки «Обзора» (изменение 025, находка №1):
раньше ёмкость считалась суммой листьев, а назначенные адреса — по всем IPv4-адресам, включая нелистовые префиксы."""
q = Prefix.__table__.alias("q")
contained = (
select(1).select_from(q)
.where(q.c.vrf_id == Prefix.vrf_id, q.c.id != Prefix.id, q.c.status == PrefixStatus.active,
func.family(q.c.prefix) == 4, q.c.prefix.op(">>")(Prefix.prefix))
.exists()
)
return select(Prefix.vrf_id, Prefix.prefix).where(Prefix.status == PrefixStatus.active, func.family(Prefix.prefix) == 4, ~contained)
@router.get("/overview", response_model=Overview, tags=["overview"])
def overview(db: Session = Depends(get_db)):
active = db.scalars(select(Prefix).where(Prefix.status == PrefixStatus.active)).all()
outs = _prefix_outs(db, list(active))
parents = {p.parent_id for p in outs if p.parent_id}
leaves4 = [p for p in outs if p.id not in parents and p.family == 4] # top_prefixes — как раньше, по листьям
top = sorted((p for p in leaves4 if p.capacity > 1), key=lambda p: p.utilization, reverse=True)[:4]
roots_stmt = _ipv4_roots(db)
roots = db.execute(roots_stmt).all()
cap = sum(capacity(str(r.prefix)) for r in roots)
roots_cte = roots_stmt.cte("overview_roots")
counts = dict(db.execute(
select(Address.status, func.count(func.distinct(Address.id)))
.select_from(Address.__table__.join(roots_cte, and_(roots_cte.c.vrf_id == Address.vrf_id, Address.address.op("<<=")(roots_cte.c.prefix))))
.where(func.family(Address.address) == 4, Address.status.in_((AddressStatus.assigned, AddressStatus.reserved)))
.group_by(Address.status)
).all())
assigned = counts.get(AddressStatus.assigned, 0)
reserved = counts.get(AddressStatus.reserved, 0)
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],
)