DOCS+HISTORY: Opus architecture prompt, Q&A, implementation plan, session logs

This commit is contained in:
2026-07-31 08:39:19 +04:00
parent 6e2418b157
commit bcacb1fb33
5 changed files with 586 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
# Prompt for Opus — Architecture: Unified Scenario System
**Ты можешь задавать уточняющие вопросы.** Если чего-то не хватает для принятия решения — спроси. Я (DeepSeek V4 Pro, ассистент Naael) отвечу.
## ПОРЯДОК ЧТЕНИЯ (обязательно прочитай в этом порядке)
**Шаг 1 — понять что такое Nubes и как работает API:**
`/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md`
**Шаг 2 — увидеть дублирование своими глазами (самое важное):**
Смотри раздел «КЛЮЧЕВОЙ КОД» ниже — там оба CREATE-флоу (ручной и сценарный) бок о бок.
**Шаг 3 — понять текущую архитектуру кода:**
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` (строки 175-270 — ручной CREATE)
- `/home/naeel/nubes/autotest/app-autotest/site/operations/scenario.py` (строки 85-210 — сценарный CREATE)
- `/home/naeel/nubes/autotest/app-autotest/site/operations/terraform.py` (общая send_params_terraform)
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_scenario_defs.py` (CRUD определений)
- `/home/naeel/nubes/autotest/app-autotest/site/db/scenario_defs.py` (SQL-функции)
- `/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py` (схема БД — найди scenario_definitions)
**Шаг 4 — понять фронтенд:**
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` (вся страница, CSS, Jinja2)
- `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js` (showParams, executeOp — как запускается ручная операция)
- `/home/naeel/nubes/autotest/app-autotest/site/static/js/scenario-form.js` (renderEditor, saveScenario — текущий редактор)
**НЕ читай — это легаси/устарело:**
- `DOCS/ARCHITECTURE.legacy.md`, `DOCS/ARCHITECTURE.md`
- `DOCS/sonnet-*.md` — переписки с другим AI
- `DOCS/gpt56-*`, `DOCS/questions-to-sol.md`, `DOCS/sol-*`
- `HISTORY/` — история сессий
- `development/` — HAR-файлы
---
## Что это за приложение
**Nubes** — облачная платформа (IaaS/PaaS). ~30 типов сервисов: VM, K8s, PostgreSQL, Redis, S3, Kafka, ClickHouse и др. Каждый сервис имеет операции: create, modify, delete, suspend, resume, redeploy.
**Autotest** — Flask-приложение, которое делает то же самое что личный кабинет Nubes, но через REST API и автоматически. Два режима:
1. **Ручной** — пользователь выбирает сервис → видит список autotest-инстансов → кликает → выбирает операцию (modify/delete/suspend/...) → заполняет параметры → запускает. Или жмёт «+ Создать» для CREATE.
2. **Сценарии** — пользователь создаёт последовательность шагов (create → modify → delete), сохраняет в БД, запускает одним кликом. Каждый шаг = атомарная операция над инстансом.
### Как работает API Nubes (CREATE)
```
1. POST /instances body={serviceId, displayName, descr} → 201, Location: ./UUID
2. POST /instanceOperations body={instanceUid, operation:"create"} → opUid
3. GET /instanceOperations/{opUid}?fields=cfsParams → параметры с defaults
4. POST /instanceOperationCfsParams (×N — все параметры)
5. GET /instanceOperations/{opUid}/validate-cfs → валидация
6. POST /instanceOperations/{opUid}/run → запуск
7. Поллинг GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,... до dtFinish
```
MODIFY/DELETE/SUSPEND/RESUME: шаг 1 пропускается, шаг 2 с `{instanceUid, svcOperationId, operation}` (с svcOperationId!).
### БД (PostgreSQL, одна на все gunicorn-воркеры)
```sql
scenario_definitions (id, client_id, stand, name, steps JSONB, version, is_active, ...)
scenario_runs (id, scenario_name, status, current_step, total_steps, error_log, app_version, ...)
runs (id, svc_id, op_name, instance_uid, status, duration_sec, params JSONB, stages JSONB, ...)
```
---
## КЛЮЧЕВОЙ КОД — ДУБЛИРОВАНИЕ CREATE-ФЛОУ
### Ручной режим (api_test.py, функция api_test)
```python
if op_name == "create":
display_name = _unique_display_name(client, display_name)
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
resp = client.post("/instances", payload)
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
if not instance_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
# ----------------------------------------------------------
op_payload = {"instanceUid": instance_uid, "operation": "create"}
op_resp = client.post("/instanceOperations", op_payload)
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not op_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
# ----------------------------------------------------------
send_params_terraform(client, op_uid, params) # ← ОБЩАЯ ФУНКЦИЯ
client.post(f"/instanceOperations/{op_uid}/run")
# ----------------------------------------------------------
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, ...), daemon=True).start()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, ...})
```
### Сценарий (scenario.py, функция run_scenario)
```python
if op_name == "create":
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
descr = f"scenario {scenario_name} step {step_num}"
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
resp = client.post("/instances", payload)
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
if not instance_uid:
raise RuntimeError(f"CREATE: no instanceUid in response: ...")
instance_map[svc_id] = instance_uid
# ----------------------------------------------------------
op_payload = {"instanceUid": instance_uid, "operation": op_name} # без svcOperationId
op_resp = client.post("/instanceOperations", op_payload)
op_uid = op_resp.get("instanceOperationUid") or _find_uid(op_resp) or _uid_from_location(...)
if not op_uid:
raise RuntimeError(f"No opUid in response: ...")
# ----------------------------------------------------------
from operations.terraform import send_params_terraform
send_params_terraform(client, op_uid, resolved_params) # ← ТА ЖЕ ФУНКЦИЯ
client.post(f"/instanceOperations/{op_uid}/run")
# ----------------------------------------------------------
# ДАЛЕЕ: синхронный while-поллинг (1800s таймаут), save_run(), _save_scenario_run()
```
**Эти два блока делают ОДНО И ТО ЖЕ.** Различаются только:
- displayName (autotest-xxx vs autotest-scenario-xxx)
- Поллинг (async thread vs sync while)
- Сохранение (runs через _finish_op vs runs + scenario_runs)
---
## КЛЮЧЕВОЙ КОД — ОГРАНИЧЕНИЕ instance_map
```python
# scenario.py, строка 91
instance_map = {} # service_id → instanceUid
# Шаг CREATE:
instance_map[svc_id] = instance_uid
# Шаг НЕ-CREATE:
instance_uid = instance_map.get(svc_id)
if not instance_uid:
raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first")
```
**Проблема:** привязано к `service_id`. Нельзя:
- Два инстанса одного сервиса в сценарии (второй CREATE перезапишет первый)
- Сослаться на инстанс из другого сценария
- Использовать существующий инстанс по UUID
---
## Что нужно спроектировать
### 1. Единый executor (operations/executor.py)
```python
def execute_operation(client, service_id, operation, instance_uid_or_none, params, display_name=None) -> dict:
"""
Единая точка входа для ручного и сценарного запуска.
Возвращает {"instance_uid": ..., "op_uid": ..., "display_name": ...}
"""
```
`api_test.py` и `scenario.py` вызывают эту функцию. Поллинг и save_run — снаружи (у каждого свой).
### 2. Гибкие ссылки на инстансы
Новый формат шага в `scenario_definitions.steps`:
```json
[
{"service_id": 1, "operation": "create", "params": {...}, "output": "d1"},
{"service_id": 1, "operation": "modify", "params": {...}, "instance_ref": "d1"},
{"service_id": 90, "operation": "create", "params": {...}, "output": "pg"},
{"service_id": 1, "operation": "delete", "params": {}, "instance_uid": "UUID-явно"}
]
```
Резолвинг на бэкенде: `output` → сохраняем в словарь `{name: instance_uid}`. `instance_ref` → берём из словаря. `instance_uid` → используем как есть.
### 3. UI редактора сценариев
**Текущее:** inline-форма в `scenario-body`, сервис = numeric input, операция = text input (БАГ), параметры = key:value строки.
**Нужно:** полноценный редактор с:
- Выпадающий список сервисов (`GET /api/services`)
- Выпадающий список операций (`GET /api/operations/{svcId}`)
- Параметры с автоподгрузкой из `/api/params/{svcOpId}`: name, type, default, valueList, dataDescriptor
- Поле `output` для create-шагов (имя для ссылок)
- Дропдаун `instance_ref` для не-create шагов (output-имена предыдущих шагов)
- `[↑][↓]` для перестановки шагов
**Вопросы:**
1. Модальное окно или раскрытие внутри `scenario-body`? Аргументируй.
2. Как показывать параметры: таблица (name|type|default|value) или упрощённо (key=value)?
3. pre-fill параметров при смене операции — авто или по кнопке?
4. Куда скроллится страница при открытии редактора?
### 4. Документация для чтения (кроме кода)
- `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` ← обязательно
- `/home/naeel/nubes/autotest/DOCS/ARCHITECTURE-FULL.md` ← общая архитектура
- `/home/naeel/nubes/autotest/DOCS/architecture-final.md` ← финальная версия
---
## Вопросы
1. **Unified executor:** сигнатура, возврат, обработка ошибок на каждом шаге CREATE-флоу
2. **Формат шагов:** как парсить `output`/`instance_ref`/`instance_uid`, валидация, резолвинг на бэкенде
3. **Схема БД:** нужны ли изменения в `scenario_definitions.steps`? Новая колонка для output-блоков?
4. **UI редактора:** модал vs inline, компоновка блоков, автоподгрузка параметров, скролл
5. **Миграция:** что делать с существующим dummy_test при смене формата шагов
6. **Порядок:** в какой последовательности реализовывать
+138
View File
@@ -0,0 +1,138 @@
# Opus Implementation Plan — Унификация сценариев autotest (2026-07-31)
> Основано на DOCS/opus-questions-2026-07-31.md (19 Q+A) и ревью пользователя.
## Цель
Устранить дублирование CREATE-флоу (ручной api_test.py vs сценарный scenario.py),
ввести единый execute_operation, гибкие ссылки на инстансы (output/instance_ref/instance_uid),
модальный редактор сценариев с богатым рендером параметров.
## Порядок реализации (E19): бэкенд → формат → миграция вызовов → UI
---
## Фаза 1 — Общие модули (фундамент)
### 1. api/utils.py (NEW)
- `find_uid(resp)` — поиск UUID по ключам: instanceOperationUid → instanceUid → uid.
- `uid_from_location(loc)` — UUID из Location-заголовка.
- Заменяет 2 дубля (в api_test.py и scenario.py — сейчас разные реализации).
### 2. operations/poll.py (NEW)
- `poll_until_done(client, op_uid, timeout=1800)` → dict {status, is_successful,
error_log, stages, duration, svc}.
- Критерий завершения: dtFinish != "" (НЕ isInProgress).
- Общий цикл для async (_finish_op) и sync (run_scenario). Убирает хардкод 1800s в 3 местах.
### 3. operations/executor.py (NEW)
- `execute_operation(client, service_id, operation, instance_uid, params,
svc_op_id=None, display_name=None)` → dict {ok, error, failed_step,
instance_uid, op_uid, display_name}.
- Делает всё ДО /run включительно: POST /instances (только create) →
POST /instanceOperations → send_params_terraform → POST /run. НЕ поллит.
- Ветвится по operation=="create" ВНУТРИ:
- create: POST /instances → op_payload {instanceUid, operation}; svc_op_id игнорируется.
- non-create: op_payload {instanceUid, svcOperationId, operation}.
- failed_step ∈ {instances, instanceOperations, params, run}.
- tracker_add вызывается ВНУТРИ executor для всех create (E3) — сразу после
получения instanceUid, до params/run (защита от сирот).
- Переиспользует существующую send_params_terraform (operations/terraform.py) как есть.
---
## Фаза 2 — Формат шагов и резолвинг (depends Фаза 1)
### 4. routes/api_scenario_defs.py — _validate_steps
Новые опциональные ключи шага: output, instance_ref, instance_uid.
Валидация:
- output уникален в пределах сценария.
- instance_ref ссылается на output из ПРЕДЫДУЩИХ шагов.
- для не-create шага обязателен instance_ref | instance_uid | (fallback старый формат).
- Старый формат (только service_id) продолжает проходить валидацию.
### 5. operations/scenario.py — резолвинг инстанса
Приоритет: instance_uid > instance_ref > instance_map[service_id] (fallback).
После create: bindings[output] = instance_uid — в память И в
scenario_runs.instance_bindings (колонка JSONB уже есть, DEFAULT '{}').
Резолвинг во время выполнения — ТОЛЬКО из памяти (БД для наблюдаемости).
---
## Фаза 3 — Миграция вызывающих (depends Фаза 1-2)
### 6. routes/api_test.py
- CMDB delete ОСТАЁТСЯ как предпроверка ПЕРЕД executor: при cmdb_ok → early return
(opUid="cmdb-...", без /run, без поллинга). Иначе → execute_operation.
- Остальные операции: заменить inline флоу на execute_operation.
- _finish_op использует poll_until_done (save_run, _op_results, tracker_remove(delete)
остаются здесь).
- Импорт find_uid/uid_from_location из api/utils.py; удалить локальные копии.
### 7. operations/scenario.py
- Заменить inline флоу на execute_operation.
- sync while-поллинг → poll_until_done.
- Удалить локальные _find_uid/_uid_from_location.
### 8. db/init_db.py — startup cleanup (E16)
В init_db() добавить:
UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart'
WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour';
(init_db вызывается из pool._ensure_schema() раз на воркер, идемпотентно).
---
## Фаза 4 — UI редактора (depends Фаза 2)
### 9. static/js/params-render.js (NEW)
Вынести из operations.js:
- renderParamRow(p, allInst) — уже чистая, без глобалов.
- renderMapFixedRow(p, dfl) — чистая.
- collectParams(containerSelector='#params-form') — параметризовать контейнер
(единственная правка сигнатуры; сейчас хардкодит #params-form).
Глобалы AUTOTEST_PREFIX/currentSvcId/makeCreateDisplayName остаются в operations.js.
_esc (utils.js) и validateJson — общие глобалы.
### 10. static/js/operations.js
Использовать общий params-render.js (удалить дубли рендера).
### 11. static/js/scenario-form.js — модальный редактор
- Модал на весь экран (не inline scenario-body — параметры map-fixed слишком тесны).
- Дропдаун сервисов (GET /api/services).
- Дропдаун операций (GET /api/operations/{svcId}).
- Авто pre-fill параметров при смене операции (GET /api/params/{svcOpId}),
показать ВСЕ параметры с defaults + кнопка «Сбросить на defaults».
- Поле output для create-шагов.
- Дропдаун instance_ref для не-create (output'ы предыдущих шагов).
- Кнопки [↑][↓] перестановки шагов.
- В БД — символические имена параметров ({"durationMs":"5000"}), резолв в numeric ID в runtime.
### 12. templates/index.html
Разметка модала + подключить params-render.js.
### 13. app.py — bump VERSION.
---
## Решения (зафиксировано)
- Схема БД без изменений (C9), только новые ключи в steps JSONB.
- Обе версии формата параллельно, без миграции данных (B8), fallback на service_id.
- _op_results в памяти — вне scope (E18).
- lock_check (один RUNNING сценарий) сохраняется (E17).
- instance_bindings (JSONB) — используется (C10).
## Verification
1. py_compile для .py, node -c для .js.
2. Ручной CREATE через UI → инстанс создан, в трекере, запись в runs.
3. Ручной DELETE → CMDB-путь работает (early return).
4. Сценарий старый формат (seed dummy_test) → работает без изменений.
5. Сценарий новый формат: create output:d1 → modify instance_ref:d1 →
delete instance_ref:d1 → все OK, instance_bindings заполнен.
6. Два create одного сервиса с разными output → два разных инстанса.
7. Валидация: instance_ref на несуществующий output → ошибка при сохранении.
8. Startup-cleanup: зависший RUNNING >1ч → TIMEOUT после рестарта.
## Файлы
NEW: operations/executor.py, operations/poll.py, api/utils.py, static/js/params-render.js
MOD: routes/api_test.py, operations/scenario.py, routes/api_scenario_defs.py,
db/init_db.py, static/js/scenario-form.js, static/js/operations.js,
templates/index.html, app.py (VERSION)
+61
View File
@@ -0,0 +1,61 @@
# Opus Questions + Answers — 2026-07-31
## A. Единый executor (operations/executor.py)
**A1. Граница executor: до /run включительно, без поллинга.**
Executor делает все шаги: POST /instances → POST /instanceOperations → send_params_terraform → /run. Возвращает `{instance_uid, op_uid, display_name}`. Поллинг — забота вызывающего (api_test.py: async thread, scenario.py: sync while).
**A2. Ошибки: dict `{ok, error, failed_step}`.**
Не исключения. `failed_step` = "instances" | "instanceOperations" | "params" | "run". Оба вызывающих конвертируют в свой формат (jsonify / RuntimeError).
**A3. Трекер: да, вызывать tracker_add внутри executor для всех create.**
Сценарные инстансы — такие же реальные инстансы в облаке, должны быть в трекере. Единообразие.
**A4. _finish_op: оставить в api_test.py.**
Но вынести цикл поллинга в общий хелпер `poll_until_done(client, op_uid, timeout=1800)` в `operations/poll.py`, который используют и _finish_op (async), и run_scenario (sync).
## B. Гибкие ссылки на инстансы
**B5. Приоритет: да. instance_uid > instance_ref > новый create.**
**B6. Хранение output→uid: и в памяти, и в БД.**
В памяти — словарь для быстрого резолвинга во время выполнения. В БД — `scenario_runs.instance_bindings` обновляется после каждого create-шага. При падении воркера видно что создалось. Но runner не зависит от БД для резолвинга (только память).
**B7. Валидация: да на всё.**
`_validate_steps()` проверяет: (a) instance_ref ссылается на output из предыдущих шагов, (b) output уникален в пределах сценария, (c) для не-create шага обязателен один из instance_ref/instance_uid.
**B8. Обратная совместимость: поддерживать оба формата, без миграции.**
Если шаг без output/instance_ref/instance_uid → fallback на старый instance_map[service_id]. Seed dummy_test работает как есть. Новые сценарии используют новый формат.
## C. Схема БД
**C9. Да, без изменений схемы.** Только новые опциональные ключи в JSONB-объектах шагов.
**C10. Да, использовать instance_bindings.** После create: `bindings[output_name] = instance_uid`. Сохранять в БД после каждого шага.
## D. UI редактора сценариев
**D11. Модальное окно на весь экран.** Параметры с dataDescriptor (map-fixed) — много вложенных полей, inline в scenario-body слишком тесно.
**D12. Да, вынести рендер параметров в общий модуль.**
`static/js/params-render.js`: `renderParamRow()`, `renderMapFixedRow()`, `collectParams()`. Используется и в operations.js, и в scenario-form.js.
**D13. Pre-fill: авто при смене операции.** Показывать ВСЕ параметры с defaults, пользователь удаляет лишние или меняет значения. Кнопка «Сбросить на defaults».
**D14. Символические имена в БД.** `{"durationMs": "5000"}` — человекочитаемо, стабильно. Резолвинг в numeric ID — в runtime.
## E. Оптимизация
**E15. Да, вынести в `api/utils.py`.** Обе реализации (_find_uid, _uid_from_location) унифицировать.
**E16. Да, startup check.** При старте: `UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart' WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'`. Просто, без доп. инфраструктуры.
**E17. Оставить lock_check.** Один сценарий за раз — безопасно. Можно ослабить позже.
**E18. Не в scope.** fallback на API работает, потеря stages — minor. Отдельная задача.
**E19. Порядок: бэкенд → UI.**
1. `operations/executor.py` + `operations/poll.py` + `api/utils.py`
2. Формат шагов (output/instance_ref) + валидация + резолвинг
3. Минимальные правки api_test.py и scenario.py (вызов executor)
4. UI редактор (модал + общий рендер параметров)