Files
ros_control/docs/changes/027-fw-downgrade/plan.md
T
ayurishchevandClaude Opus 5.5 5f7bd6588a Состояния «Upgrade FW» и осознанный откат прошивки RouterBOARD
Пункт 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>
2026-09-29 00:04:51 +03:00

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.