170 lines
11 KiB
Python
170 lines
11 KiB
Python
"""
|
||
Единый 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}
|