Files
ros_control/app/main.py
T
ayurishchevandClaude Sonnet 5 4c1841b61b ros_control: централизованное управление парком MikroTik RouterOS
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>
2026-09-19 13:13:29 +03:00

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()