85 lines
9.8 KiB
Markdown
85 lines
9.8 KiB
Markdown
# План: 027 — состояния колонки «Upgrade FW» и осознанный откат прошивки RouterBOARD
|
|||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
П. 18 ревью [2026-09-28 21:32](../../reviews/2026-09-28-2132-codebase-review.md). Прошивка RouterBOARD записывается отдельно от ROS:
|
||
|
|
`current-firmware` — записанная на плате, `upgrade-firmware` — встроенная в установленную ROS. После отката ROS (изменение 023),
|
||
|
|
например 7.24.4 → 7.23.7, плата остаётся на 7.24.4, а `upgrade-firmware` становится 7.23.7.
|
||
|
|
|
||
|
|
Приложение сравнивает прошивки на неравенство (`devices.has_fw_update`, `fw_new` в `_devices.html`), поэтому:
|
||
|
|
- колонка «Upgrade FW» показывает «↑ 7.23.7» (жёлтая метка «обновление»), хотя это откат;
|
||
|
|
- фильтры `updates=fw|any|none` и массовое «Обновить прошивку» считают такое устройство обновляемым;
|
||
|
|
- `ros.upgrade_firmware` без предупреждения запишет более старую прошивку и перезагрузит устройство.
|
||
|
|
|
||
|
|
Решения пользователя:
|
||
|
|
- объём — отображение и запрет понижения в штатном обновлении **плюс** отдельное действие «Откатить прошивку…»;
|
||
|
|
- откат прошивки — по образцу отката ROS (023): одно устройство и группа (UI и API), окно с вводом целевой версии, бэкап перед записью.
|
||
|
|
|
||
|
|
Техническая основа: `/system/routerboard/upgrade` записывает прошивку, встроенную в установленную ROS, в том числе более старую
|
||
|
|
(так MikroTik описывает откат прошивки: сначала откатить ROS, затем routerboard upgrade). Признак завершения записи — тот же
|
||
|
|
`FW_DONE_MSG` в журнале; `current-firmware` меняется только после перезагрузки.
|
||
|
|
|
||
|
|
## Изменения
|
||
|
|
|
||
|
|
### Состояние (`app/services/devices.py`, `app/ui/routes.py`)
|
||
|
|
- `fw_state(st) -> "unknown" | "update" | "downgrade" | "current"`: `unknown` — нет `fw_current` или `fw_upgrade` (CHR, не опрошено);
|
||
|
|
`update` — `version_newer(fw_upgrade, fw_current)`; `downgrade` — `version_newer(fw_current, fw_upgrade)`; иначе `current`.
|
||
|
|
По образцу `ros_state`; экспорт в шаблоны — `templates.env.globals["fw_state"]`, как `ros_state`.
|
||
|
|
- `has_fw_update(st)` → `fw_state(st) == "update"`. Фильтры `filter_devices` не меняются: `downgrade` не считается обновлением (как у ROS).
|
||
|
|
|
||
|
|
### Операции (`app/ros/operations.py`)
|
||
|
|
- Вынести из `upgrade_firmware` запись и ожидание в `_flash_firmware(c, wait_seconds, poll) -> str | None`
|
||
|
|
(ветка «уже записана после последней загрузки», `POST system/routerboard/upgrade`, ожидание `FW_DONE_MSG`, `_reboot`), без копирования.
|
||
|
|
- `upgrade_firmware`: при `version_newer(current, upgrade)` **не записывает** и возвращает сообщение
|
||
|
|
«Прошивка платы X новее встроенной в ROS Y — обновление не требуется; откат — «Откатить прошивку…»» (задача `done`, как «Прошивка актуальна»).
|
||
|
|
Массовое обновление FW такие устройства пропускает с этим сообщением.
|
||
|
|
- `check_fw_downgrade(c, target_version) -> (current, upgrade)`: не RouterBOARD → `RosError("Устройство без RouterBOARD firmware…")`;
|
||
|
|
`upgrade-firmware != target_version` → `RosError("Прошивка в ROS X не совпадает с подтверждённой Y — откат отменён")`;
|
||
|
|
не `version_newer(current, upgrade)` → `RosError("Прошивка в ROS X не старше записанной Y — это не откат")`. По образцу `check_downgrade`.
|
||
|
|
- `downgrade_firmware(c, target_version, wait_seconds=180, poll=2.0) -> str`: `check_fw_downgrade` + `_flash_firmware`;
|
||
|
|
сообщение «Откат прошивки X → Y записан, перезагрузка отправлена».
|
||
|
|
|
||
|
|
### Задачи (`app/services/ops.py`, `app/services/jobs.py`, `_jobs.html`)
|
||
|
|
- `ops.run_fw_downgrade(device_id, target_version)` — по образцу `run_ros_downgrade`: проверка → `await run_backup(device_id)`
|
||
|
|
(сбой прерывает задачу) → новое подключение, `downgrade_firmware`. Итог включает сообщение бэкапа.
|
||
|
|
- `jobs.JOB_TYPES["fw_downgrade"]`; подпись в `_jobs.html` — «Откат FW».
|
||
|
|
|
||
|
|
### API (`app/api/v1.py`)
|
||
|
|
- `POST /api/v1/devices/{id}/firmware/downgrade` — тело `DowngradeIn` → 202 `{"job_ids"}`.
|
||
|
|
- `POST /api/v1/batch/fw_downgrade` — `BatchDowngradeIn` → 202; регистрировать **до** `/batch/{action}` (как `/batch/ros_downgrade`);
|
||
|
|
в `Literal` общего `/batch/{action}` не добавлять.
|
||
|
|
|
||
|
|
### UI (`app/ui/routes.py`, `_dialog_downgrade.html`, `_devices.html`, `dashboard.html`)
|
||
|
|
- Колонка «Upgrade FW» по `fw_state`: `update` — как сейчас; `current` — «актуально»; **`downgrade`** — нейтральная `badge`
|
||
|
|
«в ROS: X» с подсказкой «Прошивка платы Y новее встроенной в ROS X. Работе обычно не мешает; откат — «Откатить прошивку…»»; `unknown` — «—».
|
||
|
|
- Меню «⋯» строки: пункт «Откатить прошивку…» (класс `mi bad`) только при `fw_state == "downgrade"`, как «Откатить ROS…».
|
||
|
|
- Меню «Обновление» панели: «Откатить прошивку до версии ROS…» рядом с «Обновить прошивку (FW)».
|
||
|
|
- Окно — **то же** `_dialog_downgrade.html`, параметризованное видом отката `kind` (`ros` | `fw`, по умолчанию `ros` — прежние адреса
|
||
|
|
работают без изменений): заголовок, колонки «записано → в ROS», текст предупреждения, поле версии. Описание видов — один словарь
|
||
|
|
в `routes.py` (функция состояния, ключи установленной/целевой версии, тип задачи, тексты), им пользуются `_downgrade_split`,
|
||
|
|
`dialog_downgrade` (`?kind=`) и `POST /ui/downgrade` (скрытое поле `kind`; неизвестное значение → 400/422). Новых шаблонов и стилей нет.
|
||
|
|
- Текст предупреждения для FW: перед записью создаётся бэкап; после записи устройство перезагрузится; прошивка берётся из установленной ROS.
|
||
|
|
|
||
|
|
## Тесты (минимально)
|
||
|
|
- `tests/test_devices.py`: `fw_state` — unknown / update / downgrade / current; `has_fw_update` ложно для `downgrade`.
|
||
|
|
- `tests/test_operations.py`, один тест по образцу `test_run_ros_downgrade_order_and_failures`: `fw_update` на устройстве в состоянии
|
||
|
|
downgrade не вызывает `routerboard/upgrade`; `fw_downgrade` с несовпавшей версией — `failed`, бэкапа и записи нет;
|
||
|
|
с совпавшей — порядок `backup → upgrade → reboot`.
|
||
|
|
- Одна проверка API: `/batch/fw_downgrade` без `target_version` → 422.
|
||
|
|
|
||
|
|
## Документация
|
||
|
|
README: «Возможности»/«Интерфейс» (состояния «Upgrade FW», откат прошивки), таблица API (два эндпоинта), типы задач, число тестов,
|
||
|
|
строка 027 в истории изменений; отчёт ревью — п. 18 со ссылкой на 027. `summary.md` — оркестратор.
|
||
|
|
|
||
|
|
## Исполнение
|
||
|
|
Исполнитель (Sonnet): код, тесты, README, пересборка стенда. Тесты не запускает, не коммитит, `.env` не читает.
|
||
|
|
**Реальную запись прошивки на устройствах не запускать** — её выполняет пользователь в UI.
|
||
|
|
|
||
|
|
## Проверка
|
||
|
|
- `venv/bin/python -m pytest -q` — все зелёные.
|
||
|
|
- Стенд (8001, `docker compose up -d --build --force-recreate`): контейнер отдаёт новый код (grep по файлу в контейнере).
|
||
|
|
- Безопасно на стенде: временное недоступное устройство `192.0.2.1` — `POST /api/v1/devices/{id}/firmware/downgrade` → 202, задача `failed`
|
||
|
|
на проверке, бэкапа нет; без `target_version` → 422; устройство удаляется.
|
||
|
|
- Ручная проверка — пользователь: колонка «Upgrade FW» на реальных устройствах («актуально»); окно отката FW (неактивная кнопка,
|
||
|
|
неверная версия, «не будут затронуты»); окно отката ROS не изменилось. Полный сценарий — откат ROS на `5G-AC-BED`,
|
||
|
|
метка «в ROS: 7.23.7», «Откатить прошивку…», возврат обычным обновлением ROS и FW.
|