docs/reviews/2026-09-28-1735-codebase-review.md — сверка с ревью 12:43 на
коммите 7c830be: закрыто 11 из 16 замечаний (п. 9 — полностью, 022),
п. 13 задокументирован с неточностью (в контейнере нет sqlite3 CLI),
открыты п. 11, 12, 14–16; новые наблюдения 17–19 (откат ROS не проверен
на устройстве, FW после отката, глубина тест-линтера). Рекомендуемый
порядок: изменение 025 — эксплуатация (п. 11, 13, 15, 16).
README: ссылка на новое ревью в «Отчётах ревью».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 KiB
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://<хост>: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-ssl8729 и неapi8728). Для доверенной сети допустим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 все клиенты видны с 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-коде.
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 | Первая версия | план · итог |
| 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 | план · итог |
Отчёты ревью
- Ревью кодовой базы 2026-09-27 (→ 018–021)
- Повторное ревью 2026-09-28 12:43 (→ 022, 024)
- Третье ревью 2026-09-28 17:35 (открыты п. 11, 12, 14–19)