Files
ayurishchevandClaude Opus 5.5 ae826fc562 Состояния колонки «Upgrade ROS» и осознанный откат ROS до версии канала
После смены канала на long-term колонка показывала «актуально», хотя
версия канала (7.23.7) старше установленной (7.24.4) — docs/changes/023:
- состояния колонки: «↑ X», «актуально», «канал: X» (версия канала старше
  установленной), «проверка не удалась» (ошибка check-for-updates больше
  не маскируется под «актуально»), «—»;
- откат ROS до версии канала как отдельная задача ros_downgrade: проверка
  версии на устройстве → обязательный бэкап (сбой прерывает) → install;
- UI: «Откатить ROS…» в меню устройства и групповой пункт в «Обновление»,
  окно с вводом целевой версии, список не затрагиваемых устройств;
- API: POST /devices/{id}/update/downgrade и /batch/ros_downgrade с
  обязательным target_version. Штатное обновление откат не выполняет.

Тесты: 33 из 33. Стенд: 422 без версии, задачи отката на недоступном
устройстве завершаются на проверке без бэкапа. Реальный откат и ручная
проверка UI пользователем на момент коммита не выполнены.

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

13 KiB
Raw Permalink Blame History

План: 023 — состояния колонки «Upgrade ROS» и осознанный откат ROS до версии канала

Context

Пользователь переключил канал обновлений на long-term (и testing), а колонка «Upgrade ROS» продолжала показывать «актуально». Разбор (оркестратор):

  • Цепочка обновления статуса корректна: ops.set_channel → refresh_status → check-for-updates (в REST синхронный — ответ приходит с итоговой latest-version, проверено на реальном устройстве) → статус сохраняется.
  • Версии каналов MikroTik сейчас: stable 7.24.4, testing 7.24.4, long-term 7.23.7, development 7.25beta5; у устройств установлена 7.24.4. На testing «актуально» верно. На long-term версия канала старше установленной: RouterOS пишет New version is available (установка = откат), а приложение по строгому сравнению (ros.version_newer, коммит 0914209) показывает «актуально» — вводит в заблуждение.
  • Попутный дефект: если последний check-for-updates завершился ошибкой (ros_check_error), а старая latest-version сохранилась, таблица тоже показывает «актуально»; ошибка видна только при пустой latest-version.

Требование пользователя: переход stable → long-term должен быть возможен как осознанная опция отката (downgrade).

Решения пользователя:

  • Подтверждение — окно с вводом целевой версии: кнопка активна только после ввода версии; сервер проверяет, что она совпадает с текущей версией канала.
  • Обязательный бэкап перед откатом; бэкап не удался — откат не выполняется.
  • Откат для одного устройства и группой (UI и API).
  • Реальный откат на устройстве запускает пользователь в UI; оркестратор проверяет всё без реального отката (тесты с имитацией RouterOS).

Техническая основа: в RouterOS 7 /system/package/update/install при версии канала старше установленной скачивает версию канала и выполняет откат (отсюда New version is available). Штатное «Upgrade ROS» (ros.install_ros_update) откат по-прежнему не выполняет.

Изменения

Отображение (app/ui/templates/_devices.html, app/ros/operations.py или app/services/devices.py)

  • Хелпер состояния (рядом с devices.has_ros_update): ros_state(st) -> "unknown" | "check_error" | "update" | "current" | "downgrade": unknown — нет ros_latest/ros_installed; check_error — есть ros_check_error; update — version_newer(latest, installed); downgrade — version_newer(installed, latest); иначе current. Экспортировать в шаблоны (как ros_newer).
  • Колонка «Upgrade ROS»: update — как сейчас («↑ X», warn); current — «актуально»; downgrade — нейтральная метка «канал: X» с подсказкой «Версия канала X старше установленной Y. Обновление не требуется; откат — пункт «Откатить ROS…»»; check_error — метка-предупреждение «проверка не удалась», текст ошибки в подсказке; unknown — «—» как сейчас.
  • Стили меток — существующие классы badge и токены; новых цветов нет.
  • Фильтр «Обновления» не меняется (downgrade не считается обновлением).

Операция отката (app/ros/operations.py, app/services/ops.py, app/services/jobs.py)

  • ros.downgrade_ros(c, target_version) -> str: check-for-updates (timeout как в install_ros_update), чтение system/package/update; если latest-version != target_version → RosError("Версия канала X не совпадает с подтверждённой Y — откат отменён"); если не version_newer(installed, latest) → RosError("Версия канала X не старше установленной Y — это не откат"); иначе POST system/package/update/install (обрыв соединения при перезагрузке — штатно, как в install_ros_update); сообщение «Откат Y → X запущен, устройство перезагрузится». Переиспользовать разбор ответа и обработку обрыва из install_ros_update (вынести общую часть, не копировать).
  • Проверка до бэкапа: вынести первую половину (check + сверка версий) в ros.check_downgrade(c, target) -> (installed, latest), чтобы не делать бэкап устройства, которое откатывать нельзя.
  • ops.run_ros_downgrade(device_id, target_version) -> str: 1) подключение, check_downgrade; 2) await run_backup(device_id) — ошибка прерывает задачу, откат не запускается (бэкап привязывается к той же задаче через ContextVar job_id); 3) новое подключение, повторная check_downgrade + install (downgrade_ros). Итоговое сообщение включает ключи бэкапа.
  • jobs.JOB_TYPES["ros_downgrade"] = ops.run_ros_downgrade; параметр target_version — через params (как channel у set_channel). Подпись типа в _jobs.html: «Откат ROS».

API (app/api/v1.py)

  • POST /api/v1/devices/{id}/update/downgrade — тело {"target_version": "7.23.7"} (обязательное, непустое) → 202 {"job_ids"}.
  • POST /api/v1/batch/ros_downgrade — BatchIn + target_version → 202 {"job_ids"}. Существующий /batch/{action} не меняется (ros_downgrade в его Literal не добавлять — откат только через эндпоинт с обязательной версией).
  • Предварительная проверка по кэшу статуса не делается в API (достоверна только проверка на устройстве в задаче); задача на устройстве с несовпадающей версией завершается failed с понятным сообщением.

UI (app/ui/routes.py, шаблоны, app/ui/static/app.js при необходимости)

  • Окно GET /ui/dialog/downgrade?device_ids=… (для одного устройства — из меню «⋯» строки, пункт «Откатить ROS…», виден только при состоянии downgrade; для группы — пункт «Откатить ROS до версии канала…» в меню «Обновление» с выбранными устройствами):
    • таблица выбранных устройств: имя, установлено → версия канала (из кэша статуса), канал;
    • устройства не в состоянии downgrade — отдельным списком «Не будут затронуты» (причина: «версия канала не старше установленной» / «нет данных проверки»);
    • если у устройств разные версии канала — откатываются только совпадающие с введённой, остальные показаны как «не будут затронуты»;
    • предупреждение: перед откатом создаётся бэкап; устройство перезагрузится; конфигурация новой версии может быть частично несовместима;
    • поле «Введите целевую версию» (input[data-enables] — существующий механизм syncEnables), кнопка «Откатить» (класс опасного действия, как «Удалить») неактивна, пока поле пусто.
  • POST /ui/downgrade: device_ids, target_version; сервер оставляет устройства, у которых в кэше ros_state == "downgrade" и ros_latest == target_version; если введённая версия не совпала ни с одним — окно остаётся открытым с ошибкой «Версия не совпадает с версией канала выбранных устройств»; иначе задачи ros_downgrade и обновление панели «Задачи» (как у групповых действий; окно закрывается — HX-Refresh или закрытие окна и обновление #jobs).
  • Разметка окна — по образцу существующих окон (_events_clear.html — окно подтверждения опасного действия, _device_form.html).

Тесты (минимально, tests/test_app.py, RouterOS — httpx.MockTransport, как в существующих тестах)

  • ros_state: update / current / downgrade / check_error / unknown.
  • run_ros_downgrade: версия не совпала → задача failed, бэкап и install не вызывались; совпала → бэкап вызван до install (порядок), install вызван; бэкап упал → install не вызывался.
  • API: target_version обязателен (422 без него); /batch/ros_downgrade → 202 и тип задачи ros_downgrade; /batch/ros_downgrade через /batch/{action} недоступен (422).
  • UI: POST /ui/downgrade с несовпадающей версией → ошибка в окне, задач нет.

Документация

README: «Возможности»/«Интерфейс» (состояния колонки, откат), таблица API (два эндпоинта), типы задач, число тестов, строка 023 в истории изменений. summary.md — оркестратор.

Исполнение

Исполнитель (Sonnet): код, тесты, README, пересборка стенда. Тесты не запускает, не коммитит, .env не читает. Реальный откат на устройствах не запускать (ни исполнителю, ни оркестратору): его выполняет пользователь в UI.

Проверка

  • pytest — все зелёные.
  • Стенд (override 8001, --force-recreate): новый код; колонка на реальных данных — устройства на stable с 7.24.4 «актуально» (при переключении пользователем на long-term — «канал: 7.23.7» и пункт «Откатить ROS…»).
  • Безопасные проверки на стенде: POST /api/v1/devices/{id}/update/downgrade для временного устройства 192.0.2.1 → 202, задача failed на проверке (устройство недоступно), бэкап не создан; target_version отсутствует → 422. Временное устройство удаляется.
  • Ручная проверка — пользователь: отображение состояний; окно отката (неактивная кнопка, неверная версия, список «не будут затронуты»); реальный откат на выбранном устройстве — с бэкапом в S3 до перезагрузки.