Files
app-autotest/site/operations/executor.py
T

170 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Единый executor операции Nubes — ЕДИНСТВЕННАЯ точка запуска операций.
Выполняет ВСЕ шаги от начала до /run (включительно):
1. CREATE: POST /instances → получает instanceUid
2. CREATE + NON-CREATE: POST /instanceOperations → получает opUid
3. Параметры: send_params_terraform → нормализация + refSvc-резолв + отправка
4. POST /instanceOperations/{opUid}/run → запуск
НЕ делает поллинг — поллинг вынесен в poll.py (используется отдельно).
Критический инвариант: tracker_add вызывается СРАЗУ после получения instanceUid
(до params и run), чтобы даже при падении gunicorn инстанс не стал сиротой.
Используется:
- api_test.py → ручной режим (фронтенд → POST /api/test → execute_operation)
- scenario.py → сценарный режим (каждый шаг → execute_operation)
"""
from api.utils import find_uid, uid_from_location
from operations.terraform import send_params_terraform
from operations.tracker import add as tracker_add
def execute_operation(client, service_id, operation, instance_uid, params,
svc_op_id=None, display_name=None, descr=None,
client_id="", stand=""):
"""Запустить операцию и вернуть результат (без поллинга).
Эта функция — «единый шлюз»: и ручной тест, и сценарий идут через неё.
Это гарантирует что поведение create/modify/delete идентично в обоих режимах.
Args:
client: HttpClient (уже с токеном и endpoint)
service_id: int — ID сервиса Nubes (напр. 1 для Болванки)
operation: str — "create" | "modify" | "delete" | "suspend" | "resume" | "redeploy"
instance_uid: str|None — UUID существующего инстанса.
None для create (инстанса ещё нет).
params: dict — {numeric_param_id: string_value}
Пример: {"198": "0", "199": "test-value"}
svc_op_id: int|None — svcOperationId (числовой ID операции в Nubes).
Для create игнорируется (там свой ID из шаблона сервиса).
display_name: str|None — displayName для create.
Если None → генерируется "autotest-{service_id}".
descr: str|None — описание инстанса (только для create).
Если None → "created by autotest".
client_id: str — ID пользователя (для tracker_add).
stand: str — "dev" / "test" (для tracker_add).
Returns:
dict с ВСЕГДА одинаковыми ключами:
{ok, error, failed_step, instance_uid, op_uid, display_name}
ok=True → успех, можно поллить
ok=False → ошибка на шаге failed_step:
"instances" — не смогли создать инстанс (POST /instances)
"instanceOperations"— не смогли создать операцию (POST /instanceOperations)
"params" — ошибка в send_params_terraform
"run" — не смогли запустить (/run)
"""
# ── Определяем режим: create или нет ──
# Все не-create операции требуют существующий instance_uid
is_create = (operation == "create")
# ═══════════════════════════════════════════════════════
# Шаг 1: POST /instances — только для create
# ═══════════════════════════════════════════════════════
# Создаём инстанс в Nubes, получаем его UUID.
# Это аналог нажатия «Создать» в личном кабинете.
if is_create:
# display_name — обязателен для create.
# Если не передан — генерируем из service_id.
if not display_name:
display_name = f"autotest-{service_id}"
# descr — описание, видно в личном кабинете.
if not descr:
descr = "created by autotest"
# payload для POST /instances:
# serviceId — какой сервис создаём
# displayName — имя инстанса (уникально в рамках пользователя)
# descr — текстовое описание
payload = {"serviceId": service_id, "displayName": display_name, "descr": descr}
try:
resp = client.post("/instances", payload)
except Exception as e:
# Сетевая ошибка или HTTP-ошибка → сразу FAIL
return {"ok": False, "error": str(e), "failed_step": "instances",
"instance_uid": None, "op_uid": None, "display_name": display_name}
# Извлекаем instanceUid из ответа API.
# Nubes API может вернуть UUID в разных местах ответа:
# 1. resp["instanceUid"] — прямой ключ
# 2. resp["instance"]["instanceUid"] — вложенный (через find_uid)
# 3. Location-заголовок — "./uuid" (через uid_from_location)
instance_uid = resp.get("instanceUid") or find_uid(resp) or uid_from_location(resp.get("_location", ""))
# Если ни один метод не дал UUID — это баг API, не можем продолжать
if not instance_uid:
return {"ok": False, "error": "No instanceUid in response", "failed_step": "instances",
"instance_uid": None, "op_uid": None, "display_name": display_name}
# ⚠️ КРИТИЧЕСКИ: tracker_add СРАЗУ после получения instanceUid.
# Зачем: если gunicorn упадёт между create и run, инстанс останется в Nubes
# но не будет виден в UI (GET /instances иногда задерживает новый инстанс).
# Трекер — краткосрочный fallback, пока облако не подхватит.
# try/except: не ронять операцию если /tmp/ переполнен или нет прав.
try:
tracker_add(client_id, stand, instance_uid, service_id, display_name)
except Exception:
pass # silently ignore — трекер не критичен для операции
# ═══════════════════════════════════════════════════════
# Шаг 2: POST /instanceOperations — создаём операцию
# ═══════════════════════════════════════════════════════
# Для create: {instanceUid, operation} — svcOperationId не нужен
# Для не-create: {instanceUid, svcOperationId, operation} — нужен ID операции
#
# Разница: для create система сама знает какой svcOperationId использовать
# (он единственный для create у каждого сервиса).
# Для modify/delete/suspend/resume — их много, нужно указать конкретный.
if is_create:
op_payload = {"instanceUid": instance_uid, "operation": operation}
else:
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": operation}
try:
op_resp = client.post("/instanceOperations", op_payload)
except Exception as e:
return {"ok": False, "error": str(e), "failed_step": "instanceOperations",
"instance_uid": instance_uid, "op_uid": None, "display_name": display_name}
# Извлекаем opUid — UUID операции, нужен для поллинга и /run
op_uid = op_resp.get("instanceOperationUid") or find_uid(op_resp) or uid_from_location(op_resp.get("_location", ""))
if not op_uid:
return {"ok": False, "error": "No opUid in response", "failed_step": "instanceOperations",
"instance_uid": instance_uid, "op_uid": None, "display_name": display_name}
# ═══════════════════════════════════════════════════════
# Шаг 3: Параметры — отправка значений CFS-параметров
# ═══════════════════════════════════════════════════════
# send_params_terraform делает 4 вещи:
# 1. Получает cfsParams шаблон операции
# 2. resolveRefSvc — подставляет UUID инстансов для refSvcId-параметров
# 3. Отправляет пользовательские значения
# 4. Отправляет defaults для незаполненных параметров + validate-cfs
try:
send_params_terraform(client, op_uid, params)
except Exception as e:
return {"ok": False, "error": str(e), "failed_step": "params",
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}
# ═══════════════════════════════════════════════════════
# Шаг 4: POST /instanceOperations/{opUid}/run — запуск
# ═══════════════════════════════════════════════════════
# После этого операция начинает выполняться в Nubes.
# Дальше — поллинг (poll_until_done), который делает вызывающий код.
try:
client.post(f"/instanceOperations/{op_uid}/run")
except Exception as e:
return {"ok": False, "error": str(e), "failed_step": "run",
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}
# Успех — все 4 шага пройдены
return {"ok": True, "error": None, "failed_step": None,
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}