Files
ros_control/docs/changes/023-ros-downgrade/plan.md
T
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

96 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: 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 до перезагрузки.