Files
ros_control/docs/changes/023-ros-downgrade/plan.md
T

95 lines
13 KiB
Markdown
Raw Normal View 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 до перезагрузки.