Рефакторинг API-слоя, операций и БД
This commit is contained in:
+103
-22
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user