From 62717b0e74d4bcdf7f3ee71caa3a7e1bcf9923ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 26 Jul 2026 11:26:28 +0400 Subject: [PATCH] Full Terraform logic: svcOperationId, /run, dtFinish, stages in UI, v1.0.41 --- site/app.py | 2 +- site/routes/api_test.py | 157 +++++++++++++++++++++++++------------- site/templates/index.html | 18 ++++- 3 files changed, 123 insertions(+), 54 deletions(-) diff --git a/site/app.py b/site/app.py index fcaf3ed..bad35cd 100644 --- a/site/app.py +++ b/site/app.py @@ -6,7 +6,7 @@ from routes.main import bp as main_bp from routes.api import bp as api_bp from routes.api_test import bp as api_test_bp -VERSION = "1.0.40" +VERSION = "1.0.41" app = Flask(__name__, template_folder="templates", static_folder="static") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc") diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 9abbd4f..bf88dc5 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -79,8 +79,8 @@ def api_params(op_id): @bp.route("/api/test", methods=["POST"]) def api_test(): - """Запустить одну операцию.""" - import threading, time + """Запустить одну операцию — логика 1:1 с Terraform.""" + import time data = request.get_json() svc_id = data["serviceId"] @@ -91,72 +91,125 @@ def api_test(): display_name = data.get("displayName", f"autotest-{svc_id}") client = _client() - - result = {"status": "RUNNING", "error": None, "instanceUid": None, "duration": 0} t0 = time.time() try: if op_name == "create": + # 1. POST /instances → instanceUid 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 - # Шаг 2: создать операцию, задать параметры, запустить - if instance_uid: - 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 op_uid: - 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") + # 2. POST /instanceOperations → opUid + 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 + + # 3. POST /instanceOperationCfsParams ×N + for pid, pval in params.items(): + client.post("/instanceOperationCfsParams", + {"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)}) + + # 4. POST /run + client.post(f"/instanceOperations/{op_uid}/run") + + # 5. waitForOperationFinish (dtFinish-based) + result = _wait_op(client, op_uid, t0) + if result["status"] == "OK": + tracker_add(instance_uid, svc_id, display_name) + result["instanceUid"] = instance_uid + return jsonify(result) - # wait - deadline = time.time() + 300 - while time.time() < deadline: - insts = get_instances(client) - for i in insts: - if i.get("instanceUid") == instance_uid: - if not i.get("operationIsInProgress") and i.get("explainedStatus") not in ("creating",): - result["status"] = "OK" - result["instanceUid"] = instance_uid - result["duration"] = round(time.time() - t0, 1) - tracker_add(instance_uid, svc_id, display_name) - return jsonify(result) - time.sleep(5) - result["status"] = "TIMEOUT" else: + # modify / suspend / delete / resume / redeploy if not instance_uid: return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400 - # все операции: {instanceUid, operation} - op_payload = {"instanceUid": instance_uid, "operation": op_name} - if params: - op_payload["cfsParams"] = [{"svcOperationCfsParamId": int(k), "paramValue": str(v)} for k, v in params.items()] - client.post("/instanceOperations", op_payload) - deadline = time.time() + 300 - while time.time() < deadline: - insts = get_instances(client) - for i in insts: - if i.get("instanceUid") == instance_uid: - if not i.get("operationIsInProgress"): - result["status"] = "OK" - result["instanceUid"] = instance_uid - result["duration"] = round(time.time() - t0, 1) - if op_name == "delete": - tracker_remove(instance_uid) - return jsonify(result) - time.sleep(5) - result["status"] = "TIMEOUT" + # redeploy: без параметров, сразу /run + 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") + result = _wait_op(client, op_uid, t0) + result["instanceUid"] = instance_uid + return jsonify(result) + + # modify/suspend/delete/resume: с параметрами + 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") + + result = _wait_op(client, op_uid, t0) + result["instanceUid"] = instance_uid + if result["status"] == "OK" and op_name == "delete": + tracker_remove(instance_uid) + return jsonify(result) + except Exception as e: import traceback - result["status"] = "FAIL" - result["error"] = str(e) + " | " + traceback.format_exc()[-200:] + return jsonify({ + "status": "FAIL", + "error": str(e) + " | " + traceback.format_exc()[-200:], + "instanceUid": instance_uid, + "duration": round(time.time() - t0, 1), + }) - result["instanceUid"] = instance_uid - result["duration"] = round(time.time() - t0, 1) - return jsonify(result) + +def _wait_op(client, op_uid, t0): + """Поллинг операции до dtFinish — 1:1 с waitForOperationFinish.""" + import time + deadline = time.time() + 300 + while time.time() < deadline: + 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") + if dt_finish and str(dt_finish).strip(): + is_ok = op.get("isSuccessful") + if not is_ok: + err = op.get("errorLog") or "неизвестная ошибка" + return {"status": "FAIL", "error": str(err), "duration": round(time.time() - t0, 1), "stages": op.get("stages", [])} + return {"status": "OK", "duration": round(time.time() - t0, 1), "stages": op.get("stages", [])} + time.sleep(5) + return {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)} + + +@bp.route("/api/test/status/") +def api_test_status(op_uid): + """Получить текущий статус операции (для поллинга с UI).""" + 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({ + "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): diff --git a/site/templates/index.html b/site/templates/index.html index 7be4a61..6639c95 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -91,6 +91,7 @@ + @@ -198,6 +199,9 @@ async function runTest(){ const btn=document.getElementById('btn-test'); btn.disabled=true; document.getElementById('test-status').textContent=' ⏳ выполняется...'; + const stagesBox=document.getElementById('stages-box'); + stagesBox.style.display='block'; + stagesBox.innerHTML=''; const params={}; document.querySelectorAll('#params-form [name^="p_"]').forEach(el=>{ @@ -220,7 +224,19 @@ async function runTest(){ const d=await r.json(); const cls=d.status==='OK'?'badge badge-success':'badge'; document.getElementById('test-status').innerHTML=` ${d.status} ${d.error||''} ${d.duration||''}s`; - if(d.status==='OK'){await selectService(SVC_ID);} + + // Показать стадии + if(d.stages&&d.stages.length){ + let sh='
Этапы
'; + d.stages.forEach(s=>{ + const ok=s.isSuccessful; + const icon=ok===true?'✅':ok===false?'❌':'⏳'; + sh+=`
${icon} ${s.stage} — ${(s.duration||0).toFixed(1)}s
`; + }); + stagesBox.innerHTML=sh; + } + + if(d.status==='OK'&&selectedOp.opName==='create'){await selectService(SVC_ID);} }catch(e){ document.getElementById('test-status').textContent=' Ошибка: '+e.message; }