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

157 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` — без шифрования, поэтому файлы содержат пароли и ключи открытым текстом. Ограничьте доступ к бакету и ссылкам скачивания.
## Запуск
```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 <API_TOKEN>`. Интерактивная документация — `/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=<evt_…>`, `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`).