""" Единый 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}