Files
ros_control/README.md
T
ayurishchevandClaude Opus 5.5 7c830be721 README по образцу ipam_control
README переверстан (docs/changes/024): быстрый старт, конфигурация одной
таблицей, архитектура с деревом каталогов, функциональность и правила по
разделам (таблица состояний «Upgrade ROS»), API по областям с
соглашениями, безопасность, эксплуатация (в т.ч. копия БД в режиме WAL,
стенд на порту 8001), интерфейс, тесты, история изменений таблицей со
ссылками на план и итог, отчёты ревью. Повторы убраны.

Все 51 ссылка на документы проверены; переменные, эндпоинты и число
тестов сверены с кодом. Код не менялся.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:32:37 +03:00

207 lines
25 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: статус и мониторинг устройств, бэкапы в S3, обновление и откат ROS,
обновление прошивки, группы устройств и журнал событий. Backend на FastAPI и SQLite, WEB UI (Jinja2 + HTMX) и JSON API используют
один сервисный слой. Дизайн и требования — `ros_control.svg`, утверждённый макет интерфейса — [изменение 006](docs/changes/006-ui-redesign/plan.md).
```
Admin Dashboard ⇄ Control Server ⇄ RouterOS REST (на каждом устройстве)
⇅ ⇅
Metadata DB S3 (Yandex Object Storage)
```
## Быстрый старт
```bash
cp .env.example .env # заполнить SECRET_KEY, SESSION_SECRET, ADMIN_PASSWORD, API_TOKEN, S3_* (см. «Конфигурация»)
docker compose up -d --build # БД — в томе ros_data, схема обновляется при старте
```
- UI: `http://<хост>:8000/`, OpenAPI: `http://<хост>: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`)
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `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 символов |
| `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)
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.
```bash
curl -s -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/api/v1/devices | python3 -m json.tool
```
Справочники: [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/).
## Безопасность
**Секреты**
- Требования — в «Конфигурации»; с небезопасными значениями приложение не стартует.
- Пароли устройств шифруются `SECRET_KEY`; секреты не пишутся ни в журнал, ни в сообщения об ошибках.
**Вход в UI**
- 5 неверных попыток за 10 минут с одного IP → IP заблокирован на 10 минут (429, пароль не проверяется); неверный пароль — 401.
- Блокировка по IP, а не по имени: единственного администратора нельзя заблокировать чужими попытками. За reverse-proxy все клиенты видны с IP прокси — нужен доверенный `X-Forwarded-For` (сейчас не поддерживается).
- Очистка журнала требует пароль; 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** через override-файл вне репозитория: порт 8000 на хосте занят другим процессом.
## Интерфейс
- Экраны: «Устройства» (вкладки групп, фильтры, таблица, карточка «Задачи»), «Бэкапы», «Группы», «Журнал».
- Пока строки не выбраны, полоса инструментов показывает фильтры; при выборе — действия над выбранными.
- Добавление и изменение устройств, группы, откат ROS, запись журнала — в окнах поверх страницы; запасные страницы `/devices/new`, `/devices/{id}/edit` работают без JavaScript.
- Меню и выпадающие списки в едином стиле «⋯», не обрезаются таблицей и раскрываются вверх у нижнего края. Списки строятся поверх скрытого `<select>`: без JavaScript работает стандартный.
- Светлая и тёмная темы — по настройке системы.
## Тесты
33 теста, фоновый опрос выключен; стенд не нужен (временная SQLite, RouterOS и S3 — заглушки). Тест-линтер не допускает синхронных обращений к БД в `async`-коде.
```bash
python3 -m venv venv && venv/bin/pip install -r requirements.txt
venv/bin/python -m pytest -q
```
Локальный запуск без Docker: `set -a; . ./.env; set +a; venv/bin/uvicorn app.main:app --reload`.
## История изменений
Каждая доработка описана в `docs/changes/<номер>/`: `plan.md` — план, `summary.md` — итог.
| № | Изменение | Документы |
|---|---|---|
| 001 | Первая версия | [план](docs/changes/001-initial-implementation/plan.md) · [итог](docs/changes/001-initial-implementation/summary.md) |
| 002 | HTTP-подключение, исправления по тесту на устройстве | [план](docs/changes/002-http-support-and-fixes/plan.md) · [итог](docs/changes/002-http-support-and-fixes/summary.md) |
| 003 | Бэкап: скачивание через REST, загрузка в S3 сервером | [план](docs/changes/003-download-via-rest-upload-from-server/plan.md) · [итог](docs/changes/003-download-via-rest-upload-from-server/summary.md) |
| 004 | Колонки Upgrade ROS/FW, меню действий «⋯» | [план](docs/changes/004-upgrade-columns-and-actions-menu/plan.md) · [итог](docs/changes/004-upgrade-columns-and-actions-menu/summary.md) |
| 005 | Группы устройств, фильтры устройств и бэкапов | [план](docs/changes/005-groups-and-filters/plan.md) · [итог](docs/changes/005-groups-and-filters/summary.md) |
| 006 | Новый интерфейс по утверждённому макету | [план](docs/changes/006-ui-redesign/plan.md) · [итог](docs/changes/006-ui-redesign/summary.md) |
| 007 | Фоновый опрос, быстрое обнаружение недоступности | [план](docs/changes/007-fast-offline-detection/plan.md) · [итог](docs/changes/007-fast-offline-detection/summary.md) |
| 008 | Примечание к устройству | [план](docs/changes/008-device-note/plan.md) · [итог](docs/changes/008-device-note/summary.md) |
| 009 | Выравнивание полей в окне устройства | [план](docs/changes/009-dialog-label-alignment/plan.md) · [итог](docs/changes/009-dialog-label-alignment/summary.md) |
| 010 | `.rsc` с `show-sensitive` | [план](docs/changes/010-export-show-sensitive/plan.md) · [итог](docs/changes/010-export-show-sensitive/summary.md) |
| 011 | Выпадающие меню не обрезаются | [план](docs/changes/011-dropdown-clipping/plan.md) · [итог](docs/changes/011-dropdown-clipping/summary.md) |
| 012 | Клик по имени устройства открывает окно изменения | [план](docs/changes/012-clickable-device-name/plan.md) · [итог](docs/changes/012-clickable-device-name/summary.md) |
| 013 | Групповое удаление бэкапов | [план](docs/changes/013-bulk-delete-backups/plan.md) · [итог](docs/changes/013-bulk-delete-backups/summary.md) |
| 014 | Поддержка CHR, строгое сравнение версий ROS | [план](docs/changes/014-chr-support/plan.md) · [итог](docs/changes/014-chr-support/summary.md) |
| 015 | Имя устройства задаётся только при создании | [план](docs/changes/015-immutable-device-name/plan.md) · [итог](docs/changes/015-immutable-device-name/summary.md) |
| 016 | Уникальные ID сущностей, журнал событий | [план](docs/changes/016-unique-ids-and-event-log/plan.md) · [итог](docs/changes/016-unique-ids-and-event-log/summary.md) |
| 017 | Журнал в UI: ротация, очистка с паролем | [план](docs/changes/017-events-ui-rotation/plan.md) · [итог](docs/changes/017-events-ui-rotation/summary.md) |
| 018 | Корректность: удаление бэкапа, миграции, групповая смена канала задачами | [план](docs/changes/018-correctness-consistency/plan.md) · [итог](docs/changes/018-correctness-consistency/summary.md) |
| 019 | Производительность: кэш бакета, БД вне event loop, один процесс на БД | [план](docs/changes/019-performance-scaling/plan.md) · [итог](docs/changes/019-performance-scaling/summary.md) |
| 020 | Выпадающие списки в стиле меню «⋯» | [план](docs/changes/020-custom-select-menus/plan.md) · [итог](docs/changes/020-custom-select-menus/summary.md) |
| 021 | Безопасность: секреты, блокировка входа, Secure-cookie, редиректы | [план](docs/changes/021-security-hardening/plan.md) · [итог](docs/changes/021-security-hardening/summary.md) |
| 022 | Остаток синхронной БД в async-коде, тест-линтер | [план](docs/changes/022-async-db-remainder/plan.md) · [итог](docs/changes/022-async-db-remainder/summary.md) |
| 023 | Состояния «Upgrade ROS», откат ROS до версии канала | [план](docs/changes/023-ros-downgrade/plan.md) · [итог](docs/changes/023-ros-downgrade/summary.md) |
| 024 | Оптимизация README | [план](docs/changes/024-readme-restructure/plan.md) · [итог](docs/changes/024-readme-restructure/summary.md) |
## Отчёты ревью
- [Ревью кодовой базы 2026-09-27](docs/reviews/2026-09-27-codebase-review.md) (→ 018–021)
- [Повторное ревью 2026-09-28](docs/reviews/2026-09-28-1243-codebase-review.md) (→ 022; открыты п. 11, 12, 14–16)