# 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. **Порядок:** в какой последовательности реализовывать