Files
autotest/DOCS/opus-architecture-prompt.md

12 KiB
Raw Permalink Blame History

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-воркеры)

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)

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)

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

# 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)

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:

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