95 lines
13 KiB
Markdown
95 lines
13 KiB
Markdown
# План: 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 до перезагрузки.
|