Рефакторинг API-слоя, операций и БД

This commit is contained in:
2026-07-31 17:13:54 +04:00
parent 82a3be2067
commit 0783319fd1
15 changed files with 986 additions and 257 deletions
+103 -22
View File
@@ -1,9 +1,20 @@
"""
Единый executor операции Nubes.
Единый executor операции Nubes — ЕДИНСТВЕННАЯ точка запуска операций.
Делает всё до /run включительно. НЕ поллит.
Вызывает tracker_add для create сразу после получения instance_uid (защита от сирот).
Используется api_test.py (ручной) и scenario.py (сценарный).
Выполняет ВСЕ шаги от начала до /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
@@ -14,75 +25,145 @@ 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
service_id: int — ID сервиса
operation: str — create/delete/modify/suspend/resume/redeploy
instance_uid: str|None — UUID существующего инстанса (None для create)
params: dict — {numeric_param_id: value}
svc_op_id: int|None — svcOperationId (игнорируется для create)
display_name: str|None — displayName (только для create)
descr: str|None — описание инстанса
client_id: str — для tracker_add
stand: str — для tracker_add
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.
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) ---
# ═══════════════════════════════════════════════════════
# Шаг 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 СРАЗУ после instance_uid, до params/run — защита от сирот
# ⚠️ КРИТИЧЕСКИ: 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 # не ронять операцию из-за трекера
pass # silently ignore — трекер не критичен для операции
# --- Шаг 2: POST /instanceOperations ---
# ═══════════════════════════════════════════════════════
# Шаг 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-6: Параметры + run ---
# ═══════════════════════════════════════════════════════
# Шаг 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}