Files
ros_control/README.md
T

28 KiB
Raw Blame History

ros_control

Централизованное управление парком MikroTik RouterOS: статус и мониторинг устройств, бэкапы в S3, обновление и откат ROS, обновление прошивки, группы устройств и журнал событий. Backend на FastAPI и SQLite, WEB UI (Jinja2 + HTMX) и JSON API используют один сервисный слой. Дизайн и требования — ros_control.svg, утверждённый макет интерфейса — изменение 006.

Admin Dashboard ⇄ Control Server ⇄ RouterOS REST (на каждом устройстве)
                       ⇅   ⇅
              Metadata DB   S3 (Yandex Object Storage)

Быстрый старт

cp .env.example .env               # заполнить SECRET_KEY, SESSION_SECRET, ADMIN_PASSWORD, API_TOKEN, S3_* (см. «Конфигурация»)
docker compose up -d --build       # БД — в томе ros_data, схема обновляется при старте
  • UI: http://<хост>:${APP_PORT:-8000}/, OpenAPI: http://<хост>:${APP_PORT:-8000}/docs.
  • Вход в UI: ADMIN_USER / ADMIN_PASSWORD из .env; API — заголовок Authorization: Bearer <API_TOKEN>.
  • Генерация секретов: python -c "import secrets; print(secrets.token_urlsafe(32))"; ключ Fernet: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())".

Конфигурация (.env)

Переменная По умолчанию Назначение
APP_PORT 8000 Порт хоста, публикуемый docker-compose.yml (читает только compose, не приложение)
APP_BIND 0.0.0.0 Адрес публикации порта; 127.0.0.1 — только за reverse-proxy на том же хосте
SECRET_KEY — Ключ Fernet для паролей устройств. Обязателен; потеря ключа — потеря доступа к сохранённым паролям
SESSION_SECRET — Подпись cookie-сессии UI. Обязателен, ≥ 32 символов
SESSION_COOKIE_SECURE false Флаг Secure у cookie сессии; включить, когда приложение работает за TLS
ADMIN_USER, ADMIN_PASSWORD admin, — Вход в UI. Пароль обязателен, ≥ 12 символов
API_TOKEN — Bearer-токен API. Обязателен, ≥ 32 символов
TRUSTED_PROXIES пусто Доверенные прокси (CIDR через запятую). Только от них принимается X-Forwarded-For для IP клиента; пусто — заголовок всегда игнорируется
DATABASE_URL sqlite:///./data/ros_control.db База метаданных (в compose — том ros_data)
S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_PREFIX Yandex Object Storage, backups Бакет резервных копий
BACKUPS_CACHE_TTL 60 Время жизни кэша списка бакета, с; 0 — без кэша
MAX_CONCURRENCY 10 Одновременные обращения к устройствам
ROS_CONNECT_TIMEOUT 4 Установление соединения, с — скорость обнаружения недоступности
ROS_TIMEOUT 30 Ответ устройства, с — долгие операции
POLL_INTERVAL 30 Период фонового опроса, с; 0 — выключить
UPDATE_CHECK_INTERVAL 1800 Как часто опрос проверяет обновления ROS, с
EVENTS_RETENTION_DAYS, EVENTS_MAX_ROWS 90, 100000 Ротация журнала по умолчанию; действующие значения меняются в UI; 0 — без ограничения

Пустые, равные change-me или слишком короткие секреты и невалидный SECRET_KEY — отказ старта (RuntimeError без значений секретов).

Архитектура

Слой Технологии
API FastAPI, pydantic v2, Bearer-токен; долгие операции — фоновые задачи с ограничением параллелизма
UI Jinja2 + HTMX, ванильный JS без сборки; сессия в cookie; шрифты IBM Plex локально (OFL), внешних ресурсов нет
БД SQLite (WAL), SQLAlchemy 2; версия схемы — PRAGMA user_version, миграции в app/migrations.py
Устройства и S3 RouterOS REST API через httpx; S3 — boto3
app/            main.py config.py db.py migrations.py models.py ids.py security.py process_lock.py s3.py
app/ros/        client.py (REST-клиент) operations.py (статус, бэкап, обновления, откат)
app/services/   devices groups backups ops jobs poller events settings rotation
app/api/        v1.py (JSON API)
app/ui/         routes.py templates/ static/ (стили, app.js, шрифты)
tests/          автотесты (pytest) — по модулям: test_security test_devices test_operations test_backups
                test_events test_ids_migrations test_architecture; helpers.py — общие хелперы, conftest.py — фикстуры
docs/changes/   планы и итоги доработок        docs/reviews/   отчёты ревью

Функциональность и правила

Устройства

  • Добавление, изменение, удаление; пароль хранится зашифрованным (Fernet) и в ответы API не попадает.
  • Имя задаётся только при создании и не меняется: оно входит в ключи бэкапов в S3. Клик по имени открывает окно изменения.
  • Примечание до 500 символов — подсказкой при наведении на имя.
  • Статус: online/offline (с причиной недоступности), модель, канал обновлений, версии ROS и FW, uptime, время запроса бэкапа.
  • Мониторинг: опрос каждые POLL_INTERVAL секунд, недоступность определяется за ~ROS_CONNECT_TIMEOUT секунд; страница обновляет статусы сама.

Колонка «Upgrade ROS»

Состояние Отображение
На канале есть более новая версия «↑ X»
Установлена версия канала «актуально»
Версия канала старше установленной (например, после перехода на long-term) «канал: X» — не обновление; откат — отдельной операцией
Последняя проверка обновлений завершилась ошибкой «проверка не удалась», текст ошибки в подсказке
Проверка ещё не выполнялась (или у устройства нет интернета) «—»

«Upgrade FW» — версия прошивки для обновления или «актуально». Устройство проверяет обновления на серверах MikroTik.

Группы и фильтры

  • Устройство — в одной группе или «Без группы». Вкладки групп со счётчиками, страница «Группы»; группу можно создать при добавлении устройства.
  • Фильтры устройств: поиск (имя, адрес, модель), статус, наличие обновлений, канал. Фильтры бэкапов: группа, устройство, тип файла, период, имя файла. Фильтры сохраняются в URL.

Операции и задачи

  • Действия по устройству — меню «⋯» строки; над выбранными — полоса инструментов; через API — и над целой группой.
  • Бэкап, обновление ROS и FW, смена канала для группы, откат ROS выполняются задачами (карточка «Задачи», /api/v1/jobs).
  • Обновление ROS: устройство перезагружается после скачивания. Обновление FW: перезагрузка, как только в журнале устройства появилась запись «Firmware upgraded successfully…».
  • Штатное обновление не выполняет откат: версия канала старше установленной — «Обновление не требуется».

Откат ROS до версии канала

  • Только осознанно: окно с вводом целевой версии, кнопка «Откатить» активна после ввода. Для одного устройства — «Откатить ROS…» в меню «⋯» (виден только в состоянии «канал: X»), для группы — пункт в меню «Обновление»; устройства, которые откатывать нельзя, показаны списком «Не будут затронуты».
  • Задача: проверка на устройстве (версия канала совпадает с введённой и старше установленной) → обязательный бэкап (сбой прерывает) → повторная проверка и установка.

Бэкапы

  • Пара файлов: .backup (без шифрования) и .rsc (с show-sensitive — пароли и ключи). Сервер создаёт их через REST, скачивает, загружает в S3 и удаляет с устройства.
  • Список бакета, скачивание по временной ссылке, удаление одного файла или выбранных (до 500 за раз).

Идентификаторы

  • <префикс>_<uuid7>: dev_ устройство, grp_ группа, job_ задача, bkp_ резервная копия, evt_ запись журнала. Типы не пересекаются, ID сортируются по времени и не переиспользуются. ID чужого типа или неверного формата — 404.
  • Резервная копия — пара файлов с одним bkp_ ID; у файлов в S3 метаданные backup-id/device-id (Yandex отдаёт Backup-Id/Device-Id). Копия без файлов в бакете помечается удалённой и остаётся в истории.

Журнал событий

  • Создание, изменение и удаление устройств и групп, перенос устройств, задачи и бэкапы, online/offline (только смены), вход в UI, ротация и очистка журнала. Запись: evt_ ID, ID сущности, актор (ui:<пользователь>, api, poller, system, anonymous), данные.
  • Страница «Журнал»: фильтры по типу, актору, устройству, периоду и тексту; «Показать ещё 100»; окно записи с полными ID и данными.
  • Ротация по сроку и числу записей (настройки в UI, хранятся в БД): при старте, раз в час и после сохранения; оставляет запись journal.rotated.
  • Очистка — только в UI, через окно с паролем пользователя; остаётся запись journal.cleared. Через API — только чтение журнала и настроек.

Требования к устройствам

  • RouterBOARD и виртуальные CHR (у CHR нет /system/routerboard: FW не показывается, обновление FW пропускается).
  • RouterOS 7.1+, сервис www-ssl (REST, порт 443; не api-ssl 8729 и не api 8728). Для доверенной сети допустим www (порт 80) со снятым флагом «HTTPS» — логин и пароль идут открытым текстом.
  • Пользователь с политиками read, write, policy, test, ftp, reboot, sensitive (проще — группа full). Группа write не подходит: бэкап падает с not enough permissions. После смены прав пересоздайте подключение к устройству.
  • Доступ устройств к S3 не нужен: файлы загружает Control Server (нужен доступ к storage.yandexcloud.net:443).

API (/api/v1)

Область Эндпоинты
Устройства GET|POST /devices (фильтры group, q, status, updates, channel), GET|PATCH|DELETE /devices/{id} (смена имени — 400), POST /devices/refresh, POST /devices/{id}/refresh
Операции POST /devices/{id}/backups, PUT /devices/{id}/update/channel, POST /devices/{id}/update/install, POST /devices/{id}/firmware/upgrade, POST /devices/{id}/update/downgrade ({"target_version"} обязателен)
Групповые POST /batch/{backup|ros_update|fw_update}, PUT /batch/channel (channel), POST /batch/ros_downgrade (target_version) — тело {"device_ids": [...]} и/или {"group_id": "grp_…"}
Группы GET|POST /groups, PATCH|DELETE /groups/{id} (устройства остаются без группы)
Бэкапы GET /backups (фильтры device_id, group, kind, date_from, date_to, q; refresh=1 — минуя кэш), GET /backups/download?key=, DELETE /backups?key=, POST /backups/delete ({"keys": [...]} → {"deleted", "failed"})
Задачи GET /jobs, GET /jobs/{id}
Журнал GET /events (фильтры entity_id, type, device_id, job_id, actor, date_from, date_to, q; before=<evt_…>, limit), GET /events/{id}, GET|PUT /events/settings

Соглашения

  • Все ID — строки с префиксом типа; group_id: null — без группы; пароль устройства передаётся только при записи.
  • Операции над устройствами (бэкап, обновления, откат, групповые) отвечают 202 {"job_ids": [...]}; результат — в задаче. Смена канала одного устройства и refresh — синхронные.
  • Ошибки: 400 — неверные данные, 401 — нет или неверный токен, 404 — нет объекта или ID чужого типа, 422 — нарушение схемы, 502 — ошибка S3.
curl -s -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/api/v1/devices | python3 -m json.tool

Справочники: RouterOS REST API, Yandex Object Storage S3 API.

Безопасность

Секреты

  • Требования — в «Конфигурации»; с небезопасными значениями приложение не стартует.
  • Пароли устройств шифруются SECRET_KEY; секреты не пишутся ни в журнал, ни в сообщения об ошибках.

Вход в UI

  • 5 неверных попыток за 10 минут с одного IP → IP заблокирован на 10 минут (429, пароль не проверяется); неверный пароль — 401.
  • Блокировка по IP, а не по имени: единственного администратора нельзя заблокировать чужими попытками. За reverse-proxy без TRUSTED_PROXIES все клиенты видны с IP прокси — одна блокировка на всех.
  • TRUSTED_PROXIES (CIDR через запятую) включает разбор X-Forwarded-For: только когда адрес соединения (peer) сам входит в доверенную сеть, заголовок берётся в расчёт — цепочка разбирается справа налево, первый адрес не из доверенной сети становится IP клиента (блокировка входа, data.ip в auth.*). Пустой список (по умолчанию) или недоверенный peer — заголовок полностью игнорируется, подделать IP нельзя. Доверяя сети Docker-моста, вы делаете доверенными и процессы хоста (не только сам reverse-proxy) — используйте узкий CIDR.
  • Очистка журнала требует пароль; 5 неверных за 10 минут блокируют её на 10 минут.
  • Редиректы после форм — только на локальный путь.

Данные

  • .rsc содержит пароли и ключи, .backup не зашифрован: ограничьте доступ к бакету и ссылкам скачивания.
  • Порт приложения не открывайте в недоверенную сеть; за TLS включите SESSION_COOKIE_SECURE=true.

Эксплуатация

  • Контейнер работает от непривилегированного пользователя (uid 10001), перезапуск unless-stopped.
  • БД (SQLite) — в томе ros_data, переживает пересборку; docker compose down -v удаляет её. Схема обновляется при старте; перед миграцией ID создаётся копия ros_control.db.bak-<метка>.
  • Один процесс на БД: файловая блокировка <файл БД>.lock; второй процесс (--workers 2+, вторая копия контейнера на том же томе) не стартует. В памяти процесса — семафор задач, счётчики неудачных паролей, кэш бакета.
  • Кэш бакета: страница «Бэкапы» и GET /api/v1/backups перечитывают бакет не чаще BACKUPS_CACHE_TTL; собственные изменения сбрасывают кэш, внешние видны по «Обновить список».
  • Копия БД: SQLite работает в режиме WAL — файл БД без -wal может быть неполным. Копировать через sqlite3 <БД> ".backup <копия>" или вместе с -wal/-shm.
  • Текущий стенд опубликован на порту 8001 — задан APP_PORT=8001 в .env стенда (порт 8000 на хосте занят другим процессом); override-файл не нужен, docker compose up -d берёт порт из .env.

Интерфейс

  • Экраны: «Устройства» (вкладки групп, фильтры, таблица, карточка «Задачи»), «Бэкапы», «Группы», «Журнал».
  • Пока строки не выбраны, полоса инструментов показывает фильтры; при выборе — действия над выбранными.
  • Добавление и изменение устройств, группы, откат ROS, запись журнала — в окнах поверх страницы; запасные страницы /devices/new, /devices/{id}/edit работают без JavaScript.
  • Меню и выпадающие списки в едином стиле «⋯», не обрезаются таблицей и раскрываются вверх у нижнего края. Списки строятся поверх скрытого <select>: без JavaScript работает стандартный.
  • Светлая и тёмная темы — по настройке системы.

Тесты

36 тестов по модулям предметных областей (tests/test_*.py), фоновый опрос выключен; стенд не нужен (временная SQLite, RouterOS и S3 — заглушки). Тест-линтер (test_architecture.py) не допускает синхронных обращений к БД в async-коде.

python3 -m venv venv && venv/bin/pip install -r requirements.txt
venv/bin/python -m pytest -q                    # все тесты
venv/bin/python -m pytest -q tests/test_backups.py   # один модуль

Локальный запуск без Docker: set -a; . ./.env; set +a; venv/bin/uvicorn app.main:app --reload.

История изменений

Каждая доработка описана в docs/changes/<номер>/: plan.md — план, summary.md — итог.

№ Изменение Документы
001 Первая версия план · итог
002 HTTP-подключение, исправления по тесту на устройстве план · итог
003 Бэкап: скачивание через REST, загрузка в S3 сервером план · итог
004 Колонки Upgrade ROS/FW, меню действий «⋯» план · итог
005 Группы устройств, фильтры устройств и бэкапов план · итог
006 Новый интерфейс по утверждённому макету план · итог
007 Фоновый опрос, быстрое обнаружение недоступности план · итог
008 Примечание к устройству план · итог
009 Выравнивание полей в окне устройства план · итог
010 .rsc с show-sensitive план · итог
011 Выпадающие меню не обрезаются план · итог
012 Клик по имени устройства открывает окно изменения план · итог
013 Групповое удаление бэкапов план · итог
014 Поддержка CHR, строгое сравнение версий ROS план · итог
015 Имя устройства задаётся только при создании план · итог
016 Уникальные ID сущностей, журнал событий план · итог
017 Журнал в UI: ротация, очистка с паролем план · итог
018 Корректность: удаление бэкапа, миграции, групповая смена канала задачами план · итог
019 Производительность: кэш бакета, БД вне event loop, один процесс на БД план · итог
020 Выпадающие списки в стиле меню «⋯» план · итог
021 Безопасность: секреты, блокировка входа, Secure-cookie, редиректы план · итог
022 Остаток синхронной БД в async-коде, тест-линтер план · итог
023 Состояния «Upgrade ROS», откат ROS до версии канала план · итог
024 Оптимизация README план · итог
025 Доверенные прокси (реальный IP клиента), параметризация порта план · итог
026 Тесты по модулям предметных областей план · итог

Отчёты ревью