from flask import Blueprint, current_app, jsonify, request from api.http_client import HttpClient, detect_endpoint from operations.get_services import get_services, get_service_detail from operations.get_instances import get_instances from operations.tracker import add as tracker_add, list_all as tracker_list from operations.tracker import remove as tracker_remove bp = Blueprint("api_test", __name__) def _client(): token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"] endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"] return HttpClient(endpoint, token) @bp.route("/api/services") def api_services(): try: raw = get_services(_client()) svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw] svc_list.sort(key=lambda s: s["svcId"]) return jsonify(svc_list) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/api/instances/list") def api_instances_list(): try: raw = get_instances(_client()) inst = [{"instanceUid": i["instanceUid"], "displayName": i["displayName"], "serviceId": i["serviceId"], "svc": i["svc"], "explainedStatus": i.get("explainedStatus", "?")} for i in raw] return jsonify(inst) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/api/operations/") def api_operations(svc_id): try: detail = get_service_detail(_client(), svc_id) ops = detail.get("operations", []) # только отслеживаемые инстансы этого сервиса tracked = tracker_list() tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id} tracked_uids = set(tracked_by_uid.keys()) instances = get_instances(_client()) nubes_uids = {i["instanceUid"] for i in instances} svc_instances = [i for i in instances if i.get("instanceUid") in tracked_uids and i.get("explainedStatus") not in ("deleted",)] # Добавить tracked инстансы, которых Nubes ещё не отдаёт (creating) for uid, t in tracked_by_uid.items(): if uid not in nubes_uids: svc_instances.append({ "instanceUid": uid, "displayName": t["displayName"], "serviceId": svc_id, "svc": detail.get("svc", ""), "explainedStatus": "creating", }) return jsonify({ "svc": detail.get("svc", ""), "operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops], "instances": svc_instances, }) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/api/params/") def api_params(op_id): try: data = _client().get(f"/instanceOperations/default/{op_id}") params = data["svcOperation"]["cfsParams"] result = [] for p in params: result.append({ "svcOperationCfsParamId": p["svcOperationCfsParamId"], "name": p.get("svcOperationCfsParam", ""), "dataType": p.get("dataType", ""), "isRequired": p.get("isRequired", False), "defaultValue": p.get("defaultValue"), "valueList": p.get("valueList"), "dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in p.get("dataDescriptor", {}).items()} if p.get("dataDescriptor") else None, }) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/api/test", methods=["POST"]) def api_test(): """Запустить операцию — возвращает opUid сразу, выполнение в фоне.""" import threading, time data = request.get_json() svc_id = data["serviceId"] op_name = data["operation"] svc_op_id = data["svcOperationId"] params = data.get("params", {}) instance_uid = data.get("instanceUid") display_name = data.get("displayName", f"autotest-{svc_id}") client = _client() try: if op_name == "create": payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""} resp = client.post("/instances", payload) instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", "")) if not instance_uid: return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500 op_payload = {"instanceUid": instance_uid, "operation": "create"} op_resp = client.post("/instanceOperations", op_payload) op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) if not op_uid: return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500 # Записать в трекер СРАЗУ после получения instanceUid, до params/run # (если params упадут — инстанс уже отслежен, сирот не будет) try: tracker_add(instance_uid, svc_id, display_name) except Exception as e: import traceback print(f"[TRACKER ERROR] add failed: {e}", flush=True) traceback.print_exc() for pid, pval in params.items(): client.post("/instanceOperationCfsParams", {"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)}) client.post(f"/instanceOperations/{op_uid}/run") # фоном ждать завершения threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), daemon=True).start() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid}) else: if not instance_uid: return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400 if op_name == "redeploy": op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name} op_resp = client.post("/instanceOperations", op_payload) op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) if not op_uid: return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500 client.post(f"/instanceOperations/{op_uid}/run") else: op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name} op_resp = client.post("/instanceOperations", op_payload) op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) if not op_uid: return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500 for pid, pval in params.items(): client.post("/instanceOperationCfsParams", {"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)}) client.post(f"/instanceOperations/{op_uid}/run") # фоном ждать завершения is_delete = (op_name == "delete") threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid}) except Exception as e: import traceback return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]}) # Результаты фоновых операций: opUid → {status, error, stages, duration} _op_results = {} def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False): """Фоном ждать dtFinish и сохранить результат.""" import time t0 = time.time() deadline = t0 + 300 while time.time() < deadline: try: try: data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages") except Exception: time.sleep(5) continue op = data.get("instanceOperation", {}) dt_finish = op.get("dtFinish") # Обновляем stages по мере появления _op_results[op_uid] = { "status": "RUNNING", "stages": op.get("stages", []), "duration": round(time.time() - t0, 1), } if dt_finish and str(dt_finish).strip(): is_ok = op.get("isSuccessful") err = op.get("errorLog") or "" print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True) # tracker_add для create уже вызван синхронно в api_test() if is_ok and is_delete: tracker_remove(instance_uid) _op_results[op_uid] = { "status": "OK" if is_ok else "FAIL", "error": str(err) if err else "", "stages": op.get("stages", []), "duration": round(time.time() - t0, 1), } return time.sleep(5) except Exception: import traceback print(f"[ERROR] _finish_op crashed: {traceback.format_exc()}", flush=True) time.sleep(5) _op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)} @bp.route("/api/test/status/") def api_test_status(op_uid): """Получить текущий статус операции (поллинг с UI).""" # сначала проверяем фоновый трекер if op_uid in _op_results: return jsonify(_op_results[op_uid]) # иначе спрашиваем API напрямую try: data = _client().get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages") op = data.get("instanceOperation", {}) dt_finish = op.get("dtFinish") done = bool(dt_finish and str(dt_finish).strip()) return jsonify({ "status": "OK" if (done and op.get("isSuccessful")) else ("FAIL" if done else "RUNNING"), "done": done, "isSuccessful": op.get("isSuccessful"), "isInProgress": op.get("isInProgress"), "duration": op.get("duration"), "stages": op.get("stages", []), "errorLog": op.get("errorLog"), }) except Exception as e: return jsonify({"error": str(e)}), 500 def _find_uid(resp): """Извлечь instanceUid или instanceOperationUid из ответа API.""" if isinstance(resp, dict): for key in ("instanceOperationUid", "instanceUid", "uid", "Uid"): v = resp.get(key) if isinstance(v, str) and v: return v return None def _uid_from_location(loc): if loc: parts = loc.rstrip("/").split("/") if len(parts[-1]) == 36: return parts[-1] return None