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>
73 lines
3.1 KiB
Python
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)
|