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

73 lines
3.1 KiB
Python

import json
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
def now() -> datetime:
return datetime.now(timezone.utc)
class Group(Base):
__tablename__ = "device_groups"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
class Device(Base):
__tablename__ = "devices"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
host: Mapped[str] = mapped_column(String(255))
port: Mapped[int] = mapped_column(Integer, default=443)
username: Mapped[str] = mapped_column(String(64))
password_enc: Mapped[str] = mapped_column(Text)
verify_tls: Mapped[bool] = mapped_column(Boolean, default=False)
use_tls: Mapped[bool] = mapped_column(Boolean, default=True, server_default="1")
group_id: Mapped[int | None] = mapped_column(Integer, nullable=True) # FK не включены — см. groups.delete_group
note: Mapped[str | None] = mapped_column(Text, nullable=True) # свободное примечание к устройству
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
# кэш последнего опроса устройства
online: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
status_json: Mapped[str] = mapped_column(Text, default="{}")
status_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
last_backup_requested_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
@property
def status(self) -> dict:
return json.loads(self.status_json or "{}")
class Backup(Base):
__tablename__ = "backups"
id: Mapped[int] = mapped_column(primary_key=True)
device_id: Mapped[int] = mapped_column(ForeignKey("devices.id", ondelete="CASCADE"))
requested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
status: Mapped[str] = mapped_column(String(16), default="running") # running|done|failed
key_binary: Mapped[str | None] = mapped_column(String(512), nullable=True)
key_rsc: Mapped[str | None] = mapped_column(String(512), nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
class Job(Base):
__tablename__ = "jobs"
id: Mapped[int] = mapped_column(primary_key=True)
device_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
device_name: Mapped[str] = mapped_column(String(64), default="")
type: Mapped[str] = mapped_column(String(32)) # backup|ros_update|fw_update
status: Mapped[str] = mapped_column(String(16), default="pending") # pending|running|done|failed
message: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)