После смены канала на 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>
13 KiB
План: 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)— ошибка прерывает задачу, откат не запускается (бэкап привязывается к той же задаче через ContextVarjob_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 до перезагрузки.