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>
46 lines
1.7 KiB
Python
46 lines
1.7 KiB
Python
"""Фоновый опрос устройств: быстрое обнаружение недоступности (period = POLL_INTERVAL)."""
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
|
|
from app.config import get_settings
|
|
from app.services import devices, ops
|
|
|
|
log = logging.getLogger("ros_control.poller")
|
|
|
|
|
|
async def cycle(last_full: dict[int, float]) -> None:
|
|
"""Один проход по всем устройствам (параллельно, не более MAX_CONCURRENCY одновременно)."""
|
|
s = get_settings()
|
|
ids = [d.id for d in devices.list_devices()]
|
|
for gone in set(last_full) - set(ids):
|
|
del last_full[gone]
|
|
sem = asyncio.Semaphore(s.max_concurrency)
|
|
started = time.monotonic()
|
|
|
|
async def one(device_id: int) -> None:
|
|
async with sem:
|
|
full = started - last_full.get(device_id, float("-inf")) >= s.update_check_interval
|
|
try:
|
|
await ops.poll_device(device_id, full)
|
|
except LookupError: # удалили во время цикла
|
|
return
|
|
except Exception: # noqa: BLE001 — сбой одного устройства не должен ронять опрос остальных
|
|
log.exception("poll failed: device %s", device_id)
|
|
return
|
|
if full:
|
|
last_full[device_id] = time.monotonic()
|
|
|
|
await asyncio.gather(*(one(i) for i in ids))
|
|
|
|
|
|
async def run() -> None:
|
|
last_full: dict[int, float] = {}
|
|
interval = get_settings().poll_interval
|
|
while True:
|
|
try:
|
|
await cycle(last_full)
|
|
except Exception: # noqa: BLE001
|
|
log.exception("poll cycle failed")
|
|
await asyncio.sleep(interval)
|