Files
ros_control/app/db.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

56 lines
1.9 KiB
Python

from contextlib import contextmanager
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.config import get_settings
class Base(DeclarativeBase):
pass
_engine = None
_SessionLocal = None
def init_db(url: str | None = None) -> None:
"""Создаёт engine и таблицы. Вызывается при старте приложения (и в тестах)."""
global _engine, _SessionLocal
url = url or get_settings().database_url
if url.startswith("sqlite:///") and ":memory:" not in url:
Path(url.removeprefix("sqlite:///")).parent.mkdir(parents=True, exist_ok=True)
_engine = create_engine(url, connect_args={"check_same_thread": False})
_SessionLocal = sessionmaker(_engine, expire_on_commit=False)
from app import models # noqa: F401 (регистрация моделей)
Base.metadata.create_all(_engine)
_migrate()
def _migrate() -> None:
"""create_all не добавляет колонки в существующие таблицы — докидываем вручную."""
with _engine.begin() as conn:
cols = {r[1] for r in conn.exec_driver_sql("PRAGMA table_info(devices)")}
if "use_tls" not in cols:
conn.exec_driver_sql("ALTER TABLE devices ADD COLUMN use_tls BOOLEAN NOT NULL DEFAULT 1")
if "group_id" not in cols:
conn.exec_driver_sql("ALTER TABLE devices ADD COLUMN group_id INTEGER")
if "note" not in cols:
conn.exec_driver_sql("ALTER TABLE devices ADD COLUMN note TEXT")
@contextmanager
def session_scope():
"""Короткая транзакция: commit при успехе, rollback при ошибке."""
session = _SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()