# 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 вида `<префикс>_`: `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` — без шифрования, поэтому файлы содержат пароли и ключи открытым текстом. Ограничьте доступ к бакету и ссылкам скачивания. ## Запуск ```bash 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` — без ограничения | ### Разработка ```bash 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 `. Интерактивная документация — `/docs`. Пример: ```bash 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=`, `limit`) | | GET/PUT | `/api/v1/events/settings` | настройки ротации журнала `{"retention_days", "max_rows"}` и сводка (очистки журнала через API нет) | Внешние справочники: [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API#RESTAPI-HTTPMethods), [Yandex Object Storage S3 API](https://yandex.cloud/ru/docs/storage/s3/api-ref/). ## Структура - `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`).