Files
ipam_control/app/main.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

82 lines
3.4 KiB
Python

import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import APIRouter, FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from app.api.v1 import auth, journal, overview, prefixes, refs
from app.config import settings
from app.db import SessionLocal
from app.models import DeviceType, Role, User
from app.request_context import RequestContextMiddleware
from app.rotation import rotation_loop
from app.security import hash_password
DEFAULT_TYPES = ["Сервер", "Сетевое оборудование", "Сетевое хранилище", "Рабочая станция", "Другое"]
def seed():
with SessionLocal() as db:
if not db.scalar(select(User.id).limit(1)) and settings.admin_password:
db.add(User(username=settings.admin_username, password_hash=hash_password(settings.admin_password), role=Role.admin))
if not db.scalar(select(DeviceType.id).limit(1)):
db.add_all(DeviceType(name=n, is_default=(n == "Другое")) for n in DEFAULT_TYPES)
db.commit()
@asynccontextmanager
async def lifespan(_: FastAPI):
seed()
task = asyncio.create_task(rotation_loop())
yield
task.cancel()
app = FastAPI(title="IPAM Manager API", version="1.0.0", lifespan=lifespan)
app.add_middleware(RequestContextMiddleware)
api = APIRouter(prefix="/api/v1")
for r in (auth.router, overview.router, refs.router, prefixes.router, journal.router):
api.include_router(r)
app.include_router(api)
@app.exception_handler(HTTPException)
async def http_error(_: Request, exc: HTTPException):
extra = exc.detail if isinstance(exc.detail, dict) else {"message": exc.detail} # dict — доп. поля (attempts_left и т.п.)
return JSONResponse({"code": exc.status_code, "fields": {}, **extra}, status_code=exc.status_code, headers=exc.headers)
@app.exception_handler(IntegrityError)
async def integrity_error(_: Request, exc: IntegrityError): # страховка: нарушение ограничения БД не должно давать 500
return JSONResponse({"code": 409, "message": "Конфликт с существующими данными", "fields": {}}, status_code=409)
@app.exception_handler(RequestValidationError)
async def validation_error(_: Request, exc: RequestValidationError):
fields = {".".join(str(x) for x in e["loc"][1:]): e["msg"].removeprefix("Value error, ") for e in exc.errors()}
return JSONResponse({"code": 422, "message": "Ошибка валидации", "fields": fields}, status_code=422)
@app.get("/healthz", include_in_schema=False)
def healthz():
return {"status": "ok"}
web = Path(__file__).resolve().parent.parent / "web"
if web.is_dir():
class RevalidatedStatic(StaticFiles):
"""Статика с обязательной ревалидацией по ETag: браузер не держит устаревший UI после обновления."""
async def get_response(self, path, scope):
response = await super().get_response(path, scope)
response.headers["Cache-Control"] = "no-cache"
return response
app.mount("/", RevalidatedStatic(directory=web, html=True), name="web")