v1.2.0: unified executor + flexible instance refs + params-render.js
Phase 1 — New modules: - api/utils.py: find_uid(), uid_from_location() (replaces 2 duplicates) - operations/poll.py: poll_until_done() (shared sync/async polling) - operations/executor.py: execute_operation() (single CREATE/non-CREATE flow) Phase 2 — Format + validation: - routes/api_scenario_defs.py: _validate_steps with output/instance_ref/instance_uid - operations/scenario.py: resolve instance_uid > instance_ref > service_id, use executor + poll_until_done, persist instance_bindings Phase 3 — Migration: - routes/api_test.py: create/non-create through executor, _finish_op through poll_until_done - db/init_db.py: startup cleanup of stuck scenario_runs (>1h) Phase 4 — UI shared module: - static/js/params-render.js: renderParamRow, renderMapFixedRow, collectParams - static/js/operations.js: use params-render.js (remove duplicates) - templates/index.html: include params-render.js - app.py: bump 1.1.57 → 1.2.0
This commit is contained in:
@@ -22,6 +22,7 @@ def _validate_steps(steps):
|
||||
"""Проверить структуру шагов сценария. Возвращает None или str с ошибкой."""
|
||||
if not isinstance(steps, list) or len(steps) == 0:
|
||||
return "Шаги: непустой массив"
|
||||
outputs = {} # output_name → step_index (для проверки ссылок и уникальности)
|
||||
for i, s in enumerate(steps):
|
||||
if not isinstance(s, dict):
|
||||
return f"Шаг {i+1}: должен быть объектом"
|
||||
@@ -33,6 +34,30 @@ def _validate_steps(steps):
|
||||
return f"Шаг {i+1}: operation обязателен"
|
||||
if not isinstance(s.get("params", {}), dict):
|
||||
return f"Шаг {i+1}: params — объект"
|
||||
|
||||
# Новые опциональные ключи: output, instance_ref, instance_uid
|
||||
out = (s.get("output") or "").strip()
|
||||
ref = (s.get("instance_ref") or "").strip()
|
||||
uid = (s.get("instance_uid") or "").strip()
|
||||
|
||||
# output — только для create, уникален
|
||||
if out:
|
||||
if op != "create":
|
||||
return f"Шаг {i+1}: output допустим только для create"
|
||||
if out in outputs:
|
||||
return f"Шаг {i+1}: output '{out}' уже используется в шаге {outputs[out]+1}"
|
||||
outputs[out] = i
|
||||
|
||||
# instance_ref — должен ссылаться на существующий output из предыдущих шагов
|
||||
if ref:
|
||||
if ref not in outputs:
|
||||
return f"Шаг {i+1}: instance_ref '{ref}' — output не найден (должен быть в create-шаге выше)"
|
||||
|
||||
# Для не-create: должен быть instance_ref, instance_uid, или старый формат (fallback)
|
||||
if op != "create":
|
||||
has_target = bool(ref or uid or (not out)) # out в не-create — ошибка выше
|
||||
# ok: ref есть, uid есть, или старый формат без новых ключей (fallback)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+55
-113
@@ -31,10 +31,13 @@ import fcntl
|
||||
|
||||
from api.http_client import HttpClient, detect_endpoint, stand_name
|
||||
from api.auth import get_token, get_client, get_client_id, get_stand, get_token_info
|
||||
from api.utils import find_uid, uid_from_location
|
||||
from operations.get_services import get_services, get_service_detail
|
||||
from operations.get_instances import get_instances
|
||||
from operations.get_params import get_params_with_current_values, _normalize_value_list
|
||||
from operations.terraform import send_params_terraform, redact_params
|
||||
from operations.executor import execute_operation
|
||||
from operations.poll import poll_until_done
|
||||
from operations.tracker import add as tracker_add, list_all as tracker_list
|
||||
from operations.tracker import remove as tracker_remove
|
||||
|
||||
@@ -190,21 +193,17 @@ def api_test():
|
||||
if not display_name:
|
||||
display_name = f"autotest-{svc_id}"
|
||||
display_name = _unique_display_name(client, display_name)
|
||||
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": 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
|
||||
result = execute_operation(
|
||||
client, svc_id, op_name, None, params,
|
||||
svc_op_id=svc_op_id, display_name=display_name
|
||||
)
|
||||
if not result["ok"]:
|
||||
return jsonify({"status": "FAIL", "error": result["error"]}), 500
|
||||
instance_uid = result["instance_uid"]
|
||||
op_uid = result["op_uid"]
|
||||
display_name = result["display_name"]
|
||||
|
||||
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(get_client_id(), get_stand(), instance_uid, svc_id, display_name)
|
||||
except Exception as e:
|
||||
@@ -212,12 +211,7 @@ def api_test():
|
||||
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
|
||||
traceback.print_exc()
|
||||
|
||||
# Отправка параметров (шаги 3-7 Terraform)
|
||||
send_params_terraform(client, op_uid, params)
|
||||
|
||||
client.post(f"/instanceOperations/{op_uid}/run")
|
||||
|
||||
# фоном ждать завершения
|
||||
# Фоновый поллинг
|
||||
user_email = get_token_info().get("email", "")
|
||||
app_version = current_app.config.get("VERSION", "")
|
||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True, get_client_id(), get_stand(), False, params, user_email, app_version), daemon=True).start()
|
||||
@@ -240,20 +234,21 @@ def api_test():
|
||||
return jsonify({"status": "OK", "opUid": "cmdb-"+instance_uid[:8], "instanceUid": instance_uid, "displayName": display_name})
|
||||
|
||||
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 = execute_operation(
|
||||
client, svc_id, op_name, instance_uid, {},
|
||||
svc_op_id=svc_op_id
|
||||
)
|
||||
if not result["ok"]:
|
||||
return jsonify({"status": "FAIL", "error": result["error"]}), 500
|
||||
op_uid = result["op_uid"]
|
||||
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
|
||||
send_params_terraform(client, op_uid, params)
|
||||
client.post(f"/instanceOperations/{op_uid}/run")
|
||||
result = execute_operation(
|
||||
client, svc_id, op_name, instance_uid, params,
|
||||
svc_op_id=svc_op_id
|
||||
)
|
||||
if not result["ok"]:
|
||||
return jsonify({"status": "FAIL", "error": result["error"]}), 500
|
||||
op_uid = result["op_uid"]
|
||||
|
||||
# фоном ждать завершения
|
||||
is_delete = (op_name == "delete")
|
||||
@@ -304,78 +299,43 @@ def _get_instance_display_name(client, instance_uid):
|
||||
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False, params=None, user_email="", app_version=""):
|
||||
"""Фоном ждать dtFinish и сохранить результат."""
|
||||
import time
|
||||
_cleanup_op_results() # очистить старые записи перед добавлением новой
|
||||
_cleanup_op_results()
|
||||
t0 = time.time()
|
||||
deadline = t0 + 1800
|
||||
while time.time() < deadline:
|
||||
|
||||
# Поллинг через общий модуль
|
||||
poll_result = poll_until_done(client, op_uid)
|
||||
|
||||
# Обновить _op_results для UI
|
||||
_op_results[op_uid] = {
|
||||
"status": poll_result["status"],
|
||||
"displayName": display_name,
|
||||
"error": poll_result["error_log"],
|
||||
"stages": poll_result["stages"],
|
||||
"duration": poll_result["duration"],
|
||||
"_ts": time.time(),
|
||||
}
|
||||
|
||||
# tracker_remove при успешном delete
|
||||
if poll_result["status"] == "OK" and is_delete:
|
||||
try:
|
||||
try:
|
||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages,svc")
|
||||
except Exception:
|
||||
time.sleep(5)
|
||||
continue
|
||||
op = data.get("instanceOperation", {})
|
||||
dt_finish = op.get("dtFinish")
|
||||
# Обновляем stages по мере появления
|
||||
_op_results[op_uid] = {
|
||||
"status": "RUNNING",
|
||||
"displayName": display_name,
|
||||
"stages": op.get("stages", []),
|
||||
"duration": round(time.time() - t0, 1),
|
||||
"_ts": time.time(),
|
||||
}
|
||||
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(client_id, stand, instance_uid)
|
||||
_op_results[op_uid] = {
|
||||
"status": "OK" if is_ok else "FAIL",
|
||||
"displayName": display_name,
|
||||
"error": str(err) if err else "",
|
||||
"stages": op.get("stages", []),
|
||||
"duration": round(time.time() - t0, 1),
|
||||
"_ts": time.time(),
|
||||
}
|
||||
# Сохранить в БД историю
|
||||
try:
|
||||
from db.save_run import save_run
|
||||
saved_params = redact_params(params or {})
|
||||
save_run(client_id, stand,
|
||||
user_email,
|
||||
svc_id, op.get("svc", ""), op_name, svc_op_id,
|
||||
op_uid, instance_uid, display_name,
|
||||
"OK" if is_ok else "FAIL",
|
||||
round(time.time() - t0, 1),
|
||||
str(err) if err else "",
|
||||
saved_params,
|
||||
op.get("stages", []),
|
||||
app_version)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[DB] save_run FAILED: {e}", flush=True)
|
||||
traceback.print_exc()
|
||||
return
|
||||
time.sleep(5)
|
||||
tracker_remove(client_id, stand, instance_uid)
|
||||
except Exception:
|
||||
import traceback
|
||||
print(f"[ERROR] _finish_op crashed: {traceback.format_exc()}", flush=True)
|
||||
time.sleep(5)
|
||||
_op_results[op_uid] = {"status": "TIMEOUT", "displayName": display_name, "duration": round(time.time() - t0, 1), "_ts": time.time()}
|
||||
# Сохранить TIMEOUT в БД
|
||||
pass
|
||||
|
||||
# Сохранить в БД
|
||||
try:
|
||||
from db.save_run import save_run
|
||||
saved_params = redact_params(params or {})
|
||||
save_run(client_id, stand, user_email,
|
||||
svc_id, "", op_name, svc_op_id,
|
||||
svc_id, poll_result["svc"], op_name, svc_op_id,
|
||||
op_uid, instance_uid, display_name,
|
||||
"TIMEOUT", round(time.time() - t0, 1),
|
||||
"TIMEOUT after 1800s", saved_params, [], app_version)
|
||||
poll_result["status"],
|
||||
poll_result["duration"],
|
||||
poll_result["error_log"],
|
||||
saved_params, poll_result["stages"], app_version)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[DB] TIMEOUT save_run FAILED: {e}", flush=True)
|
||||
print(f"[DB] save_run FAILED: {e}", flush=True)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@@ -459,21 +419,3 @@ def api_history():
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user