Control Server (FastAPI) с WEB UI (Jinja2 + HTMX) и JSON API для группового администрирования устройств RouterOS 7 через REST. - Устройства: список, статус (модель, канал, версии ROS/FW, uptime, доступные обновления), примечания, пароли шифруются (Fernet); фоновый опрос каждые 30 с и быстрое обнаружение недоступности (таймаут соединения 4 с). - Группы устройств и фильтры (устройства, резервные копии). - Резервные копии: .backup и .rsc (show-sensitive) создаются через REST, скачиваются сервером и загружаются в S3 (Yandex Object Storage). - Обновление ROS/FW и выбор канала, групповые операции задачами. - Интерфейс по утверждённому макету: светлая/тёмная темы, окна, шрифты IBM Plex локально. Docker Compose, SQLite в томе, минимальный набор тестов. - Планы и итоги каждого изменения — в docs/changes/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
import asyncio
|
|
from contextlib import asynccontextmanager, suppress
|
|
from pathlib import Path
|
|
|
|
from botocore.exceptions import BotoCoreError, ClientError
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
from app.api import v1
|
|
from app.config import get_settings
|
|
from app.db import init_db
|
|
from app.services import jobs, poller
|
|
from app.ui import routes as ui
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
init_db()
|
|
jobs.fail_stale_jobs()
|
|
task = asyncio.create_task(poller.run()) if get_settings().poll_interval > 0 else None
|
|
yield
|
|
if task:
|
|
task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await task
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
app = FastAPI(title="ros_control", lifespan=lifespan)
|
|
app.add_middleware(
|
|
SessionMiddleware, secret_key=get_settings().session_secret,
|
|
same_site="strict", https_only=False,
|
|
)
|
|
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "ui" / "static"), name="static")
|
|
app.include_router(v1.router)
|
|
app.include_router(ui.router)
|
|
|
|
@app.exception_handler(ui.LoginRequired)
|
|
async def _login(_: Request, e: ui.LoginRequired):
|
|
return RedirectResponse("/login", status_code=303)
|
|
|
|
@app.exception_handler(LookupError)
|
|
async def _404(_: Request, e: LookupError):
|
|
return JSONResponse({"detail": str(e).strip("'\"")}, status_code=404)
|
|
|
|
@app.exception_handler(ValueError)
|
|
async def _400(_: Request, e: ValueError):
|
|
return JSONResponse({"detail": str(e)}, status_code=400)
|
|
|
|
@app.exception_handler(BotoCoreError)
|
|
@app.exception_handler(ClientError)
|
|
async def _502(_: Request, e: Exception):
|
|
return JSONResponse({"detail": f"S3: {e}"}, status_code=502)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|