Files
ros_control/README.md
T
ayurishchevandClaude Opus 5.5 123b5abdfc Ревью кодовой базы и исправления корректности по его итогам
Ревью кодовой базы: docs/reviews/2026-09-27-codebase-review.md.

Корректность и согласованность, пункты 5–7 ревью (docs/changes/018):
- одиночное удаление бэкапа в UI идёт через общий delete_many: пометка
  deleted_at и событие backup.deleted, как у группового удаления и API;
- единая система миграций: ручные ALTER из db._migrate перенесены в
  migrations.run (при user_version < 1, до замены ID);
- групповая смена канала выполняется фоновыми задачами set_channel;
  PUT /api/v1/batch/channel → 202 {"job_ids": [...]} (ломающее изменение
  API), меню «Канал» в UI выводит задачи в панель «Задачи».

Тесты: 22 из 22. Стенд проверен на порту 8001 (8000 занят посторонним
процессом), боевые данные не изменены. Ручная проверка UI пользователем
на момент коммита не подтверждена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:23:34 +03:00

20 KiB
Raw Blame History

ros_control

Централизованное управление парком MikroTik RouterOS: WEB UI и JSON API. Дизайн и требования — ros_control.svg; макет интерфейса — утверждён и перенесён в приложение (см. docs/changes/006-ui-redesign).

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

Возможности

Устройства

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

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

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

Операции

  • Действия по устройству — в меню «⋯» строки; действия над выбранными устройствами (бэкап, статус, обновление ROS/FW, канал, перенос в группу) появляются в полосе инструментов при выборе строк; через API их можно запускать и над всей группой.
  • Бэкап: .backup (без шифрования) и .rsc (с show-sensitive, т.е. с паролями и ключами — из него можно восстановить все сущности, использующие пароль). Сервер создаёт файлы через REST, скачивает во временную папку, загружает в S3 и удаляет файлы с устройства.
  • Список бэкапов в бакете, скачивание (временная ссылка), удаление одного файла или группы выбранных (чекбоксы, кнопка «Удалить» с подтверждением).
  • Канал обновлений; обновление ROS (устройство перезагружается после скачивания); обновление FW (перезагрузка сразу после появления в журнале устройства записи «Firmware upgraded successfully…»).
  • Долгие и групповые операции выполняются задачами (карточка «Задачи», состояние — в UI и через API).

Идентификаторы и журнал событий

У каждой сущности глобально уникальный ID вида <префикс>_<uuid7>: dev_… устройство, grp_… группа, job_… задача, bkp_… резервная копия, evt_… запись журнала. Префикс исключает пересечение типов, UUIDv7 не повторяется и сортируется по времени создания; ID не переиспользуются после удаления. Числовых ID нет: GET /api/v1/devices/dev_…, ID чужого типа или неверного формата — 404. Резервная копия — пара файлов (.backup + .rsc) с одним bkp_ ID; файлы в бакете имеют метаданные backup-id/device-id (Yandex возвращает их как Backup-Id/Device-Id), в списке файлов API есть backup_id; копия, у которой пропали файлы, помечается удалённой, но остаётся в истории. Журнал событий пишется в БД: создание, изменение и удаление устройств и групп, перенос устройств, жизненный цикл задач и бэкапов, смена online/offline, вход в UI, ротация и очистка журнала. Каждая запись содержит evt_ ID, ID сущности, актора (ui:<пользователь>, api, poller, system) и данные. В UI — страница «Журнал» (фильтры по типу, актору, устройству, периоду и поиску по сообщению/ID; подгрузка «Показать ещё 100»; клик по строке открывает запись с полными ID и данными).

  • Ротация: «Журнал → Настройки» — срок хранения (дней) и максимум записей, 0 — без ограничения. Настройки хранятся в БД (значения по умолчанию — EVENTS_RETENTION_DAYS=90, EVENTS_MAX_ROWS=100000 в .env); ротация выполняется при старте, раз в час и сразу после сохранения настроек и оставляет запись journal.rotated.
  • Очистка: «Журнал → Очистить журнал» открывает окно с вводом пароля пользователя. Неверный пароль журнал не трогает (journal.clear_denied), 5 неверных попыток за 10 минут блокируют очистку на 10 минут. После успешной очистки остаётся одна запись journal.cleared (кто и сколько удалил). Очистки через API нет — только через это окно; API даёт чтение журнала и настроек.

Интерфейс

Экран строится сверху вниз: приложение (навигация) → страница (заголовок, счётчики, главное действие) → вкладки групп → полоса инструментов таблицы → данные; «Задачи» — отдельная карточка. Пока ничего не выбрано, полоса показывает фильтры; при выборе строк — действия над выбранными. Добавление и изменение устройств, создание и переименование групп — в окнах поверх страницы (запасные страницы /devices/new, /devices/{id}/edit работают без JavaScript). Выпадающие меню не обрезаются таблицей и раскрываются вверх, если снизу нет места. Светлая и тёмная темы переключаются по настройке системы. Шрифты IBM Plex лежат в app/ui/static/fonts (лицензия OFL), внешние ресурсы не загружаются.

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

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

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

  • Замените значения по умолчанию API_TOKEN и SESSION_SECRET на случайные (openssl rand -hex 24); порт 8000 не стоит открывать в недоверенную сеть.
  • Пароли устройств шифруются ключом SECRET_KEY (Fernet); потеря ключа = потеря доступа к сохранённым паролям.
  • Секреты в бэкапах: .rsc создаётся с show-sensitive, а .backup — без шифрования, поэтому файлы содержат пароли и ключи открытым текстом. Ограничьте доступ к бакету и ссылкам скачивания.

Запуск

cp .env.example .env    # заполнить SECRET_KEY, ADMIN_PASSWORD, API_TOKEN, S3_*
docker compose up -d --build     # UI: http://localhost:8000, OpenAPI: /docs

БД (SQLite) хранится в Docker-томе ros_data (docker volume inspect ros_control_ros_data), переживает пересборку контейнера; схема обновляется автоматически при старте (перед миграцией ID создаётся копия ros_control.db.bak-<метка> рядом с БД). Остановка: docker compose down (том сохраняется; down -v удалит БД).

Ключ Fernet: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())".

Настройки (.env)

Переменная По умолчанию Назначение
SECRET_KEY — ключ Fernet для паролей устройств (обязателен)
SESSION_SECRET change-me подпись cookie-сессии UI
ADMIN_USER / ADMIN_PASSWORD admin / change-me вход в UI
API_TOKEN change-me Bearer-токен API
DATABASE_URL sqlite:///./data/ros_control.db база метаданных (в compose — том)
S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_PREFIX Yandex Object Storage, backups бакет для резервных копий
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 — без ограничения

Разработка

python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
set -a; . ./.env; set +a
./venv/bin/uvicorn app.main:app --reload
./venv/bin/python -m pytest          # 22 теста, фоновый опрос в тестах выключен

API v1

Заголовок Authorization: Bearer <API_TOKEN>. Интерактивная документация — /docs. Пример:

curl -s -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/api/v1/devices | python3 -m json.tool

Все id — строки (dev_…, grp_…, job_…); group_id — grp_…. Устройство: name, host, port, username, password (только при записи), verify_tls, use_tls (false — HTTP), group_id (null — без группы), note.

Метод Путь Назначение
GET/POST /api/v1/devices список (фильтры group, q, status, updates, channel) / добавить
GET/PATCH/DELETE /api/v1/devices/{id} получить / изменить (имя менять нельзя — 400) / удалить
POST /api/v1/devices/refresh, /devices/{id}/refresh обновить статус
POST /api/v1/devices/{id}/backups бэкап (задача)
PUT /api/v1/devices/{id}/update/channel {"channel": "stable|long-term|testing|development"}
POST /api/v1/devices/{id}/update/install обновление ROS (задача)
POST /api/v1/devices/{id}/firmware/upgrade обновление FW (задача)
GET/POST /api/v1/groups группы (с числом устройств) / создать
PATCH/DELETE /api/v1/groups/{id} переименовать / удалить (устройства остаются без группы)
POST /api/v1/batch/{backup|ros_update|fw_update} {"device_ids": [...]} и/или {"group_id": N} — групповая операция (задача)
PUT /api/v1/batch/channel {"device_ids": [...] или "group_id": N, "channel": "..."} — групповая смена канала (задача set_channel) → 202 {"job_ids": [...]}
GET/DELETE /api/v1/backups, /backups/download?key= бэкапы в бакете (фильтры device_id, group, kind, date_from, date_to, q)
POST /api/v1/backups/delete групповое удаление файлов: {"keys": [...]} → {"deleted": N, "failed": M}
GET /api/v1/jobs, /jobs/{id} состояние задач
GET /api/v1/events, /events/{id} журнал событий (фильтры entity_id, type, device_id, job_id, actor, date_from, date_to, q; постранично before=<evt_…>, limit)
GET/PUT /api/v1/events/settings настройки ротации журнала {"retention_days", "max_rows"} и сводка (очистки журнала через API нет)

Внешние справочники: RouterOS REST API, Yandex Object Storage S3 API.

Структура

  • app/ros — клиент RouterOS REST и операции (статус, бэкап, скачивание файлов, обновления)
  • app/s3.py — бакет; app/security.py — шифрование и авторизация; app/models.py, app/db.py — БД и миграции
  • app/services — устройства и фильтры, группы, бэкапы, операции, задачи, фоновый опрос (poller.py)
  • app/api — JSON API; app/ui — WEB UI (Jinja2 + HTMX, static/ со стилями, скриптом и шрифтами)
  • tests — минимальный набор; docs/changes — планы и итоги каждого изменения
  • docs/reviews — ревью кодовой базы (замечания и приоритеты исправлений)

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

Планы и итоги — в docs/changes/:

  • 001-initial-implementation — первая версия.
  • 002-http-support-and-fixes — HTTP-подключение, исправления по итогам теста на реальном устройстве.
  • 003-download-via-rest-upload-from-server — бэкап: скачивание файлов через REST, загрузка в S3 сервером.
  • 004-upgrade-columns-and-actions-menu — колонки Upgrade ROS/FW, меню действий «⋯».
  • 005-groups-and-filters — группы устройств, фильтры устройств и бэкапов.
  • 006-ui-redesign — новый интерфейс по утверждённому макету; исправлены перезагрузка после обновления FW и привязка группы при добавлении устройства.
  • 007-fast-offline-detection — фоновый опрос устройств (30 с), быстрый таймаут соединения, автообновление страницы.
  • 008-device-note — примечание к устройству.
  • 009-dialog-label-alignment — выравнивание полей в окне устройства.
  • 010-export-show-sensitive — .rsc создаётся с show-sensitive (секреты в файле).
  • 011-dropdown-clipping — выпадающие меню не обрезаются в узком окне.
  • 012-clickable-device-name — клик по имени устройства открывает окно изменения.
  • 013-bulk-delete-backups — выбор файлов чекбоксами и групповое удаление бэкапов.
  • 014-chr-support — поддержка CHR (нет /system/routerboard), строгое сравнение версий ROS, причина недоступности в таблице.
  • 015-immutable-device-name — имя устройства задаётся только при создании.
  • 016-unique-ids-and-event-log — глобально уникальные ID (префикс + UUIDv7) для всех сущностей, журнал событий в БД, миграция числовых ID.
  • 017-events-ui-rotation — страница «Журнал» в UI, настройки ротации, очистка журнала с подтверждением паролем.
  • 018-correctness-consistency — одиночное удаление бэкапа в UI через общий сервис delete_many, единая система миграций (колонки старой схемы — в migrations.run до миграции ID), групповая смена канала фоновыми задачами (set_channel).