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>
50 lines
2.5 KiB
Python
50 lines
2.5 KiB
Python
"""Список бэкапов из бакета с фильтрами (устройство, группа, тип, даты, текст)."""
|
|
from datetime import date
|
|
|
|
from app import s3
|
|
from app.services import devices, groups
|
|
|
|
|
|
def _kind(key: str) -> str:
|
|
return "backup" if key.endswith(".backup") else "rsc" if key.endswith(".rsc") else "other"
|
|
|
|
|
|
async def search(device: str = "", group: str = "", kind: str = "", date_from: date | None = None,
|
|
date_to: date | None = None, q: str = "") -> tuple[list[dict], dict]:
|
|
"""Отфильтрованные бэкапы и итоги по всему бакету ({"total": N, "size": байты}).
|
|
Ключи вида backups/<устройство>/<файл>: устройство берётся из ключа, группа — по имени устройства.
|
|
group: "" — все, "none" — без группы (в т.ч. бэкапы удалённых устройств), иначе id группы.
|
|
Даты — по времени изменения объекта (UTC), включительно."""
|
|
group_names = {g["id"]: g["name"] for g in groups.list_groups()}
|
|
device_group = {d.name: group_names.get(d.group_id) for d in devices.list_devices()}
|
|
want_group = group_names.get(int(group)) if group.isdigit() else None
|
|
|
|
everything = await s3.list_backups(None)
|
|
stats = {"total": len(everything), "size": sum(i["size"] for i in everything)}
|
|
q = q.strip().lower()
|
|
out = []
|
|
for item in everything:
|
|
parts = item["key"].split("/")
|
|
name = parts[1] if len(parts) >= 3 else ""
|
|
item = {**item, "device": name, "group": device_group.get(name), "kind": _kind(item["key"])}
|
|
if device and name != device:
|
|
continue
|
|
if group == "none" and item["group"] is not None:
|
|
continue
|
|
if group.isdigit() and (want_group is None or item["group"] != want_group):
|
|
continue
|
|
if kind and item["kind"] != kind:
|
|
continue
|
|
day = item["last_modified"].date()
|
|
if (date_from and day < date_from) or (date_to and day > date_to):
|
|
continue
|
|
if q and q not in item["key"].lower():
|
|
continue
|
|
out.append(item)
|
|
return out, stats
|
|
|
|
|
|
async def list_backups(device: str = "", group: str = "", kind: str = "", date_from: date | None = None,
|
|
date_to: date | None = None, q: str = "") -> list[dict]:
|
|
return (await search(device, group, kind, date_from, date_to, q))[0]
|