Пункт 18 ревью 2026-09-28 21:32 (docs/changes/027): после отката ROS
плата остаётся на более новой прошивке, а приложение сравнивало версии
на неравенство и показывало откат как обновление.
- devices.fw_state: unknown/update/downgrade/current; has_fw_update и
фильтры не считают откат обновлением; метка «в ROS: X».
- upgrade_firmware не понижает прошивку; общая запись — _flash_firmware.
- Задача fw_downgrade: проверка → обязательный бэкап → запись →
перезагрузка; API POST /devices/{id}/firmware/downgrade и
/batch/fw_downgrade (target_version обязателен).
- UI: «Откатить прошивку…» (строка и группа), общее окно отката с kind.
Тесты: 39 из 39. Стенд: временное недоступное устройство — 422/202,
задачи failed на проверке, бэкапов нет. Реальная запись прошивки при
откате и ручная проверка UI не выполнялись.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.8 KiB
План: 027 — состояния колонки «Upgrade FW» и осознанный откат прошивки RouterBOARD
Context
П. 18 ревью 2026-09-28 21:32. Прошивка 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.