Files
app-autotest/site/operations/scenario.py
T
naeel a96658b9b5 v1.1.55: split large files + CRUD scenario editor
Backend (5→7 files):
- api_test.py: 622→479, terraform functions → operations/terraform.py (142)
- api_scenario.py: 241→split into run (153) + defs (108) with _validate_steps()
- db/scenario_defs.py: +client_id, stand, is_seed in list_definitions

Frontend (1→10 files):
- app.js: 554→16 (loader), split into:
  utils.js (45), instances.js (89), operations.js (249),
  history.js (35), scenario-list.js (90)
- New CRUD: scenario-form.js (128), create (25), edit (12), delete (13)

Max file size: JS 249, PY 479
2026-07-30 22:02:22 +04:00

213 lines
9.0 KiB
Python

"""
Scenario runner — шаги из БД → API → результат.
Формат шага:
{ "service_id": 1, "operation": "create", "params": { "durationMs": "5000" } }
Для каждого шага:
1. service_id уже в шаге
2. operation имя → svcOperationId (из GET /services/{id})
3. param коды → numeric IDs (из GET /instanceOperations/default/{opId})
4. create: новый инстанс; остальные: переиспользовать по service_id
5. Выполнить через API (как в api_test)
6. Сохранить результат в runs + scenario_runs
"""
import json
import time
import uuid
from operations.get_services import get_service_detail
from db.pool import get_conn, put_conn
from db.save_run import save_run
AUTOTEST_PREFIX = "autotest-scenario-"
def _find_uid(resp):
"""Extract UUID from nested response (e.g. {instanceOperation: {instanceOperationUid: ...}})."""
if isinstance(resp, dict):
for v in resp.values():
if isinstance(v, dict):
uid = v.get("instanceOperationUid") or v.get("instanceUid")
if uid:
return uid
return None
def _uid_from_location(loc):
"""Extract UUID from Location header."""
if not loc:
return None
parts = str(loc).rstrip("/").split("/")
return parts[-1] if parts[-1] else None
def _resolve_params(cfs_params, symbolic_params):
"""Символические коды → {numeric_id: value}."""
code_to_id = {p.get("svcOperationCfsParam", ""): p["svcOperationCfsParamId"] for p in cfs_params}
result = {}
for code, val in symbolic_params.items():
pid = code_to_id.get(code)
if pid is None:
raise ValueError(f"Param code '{code}' not found in operation template")
result[str(pid)] = str(val)
return result
def _save_scenario_run(scenario_run_id, status, current_step=0, duration_sec=None, error_log=None):
"""Обновить запись scenario_runs."""
conn = get_conn()
if not conn:
return
try:
cur = conn.cursor()
cur.execute("""
UPDATE scenario_runs
SET status = %s, current_step = %s, duration_sec = %s, error_log = %s
WHERE id = %s
""", (status, current_step, duration_sec, error_log, scenario_run_id))
conn.commit()
cur.close()
except Exception as e:
print(f"[SCENARIO] save_scenario_run error: {e}", flush=True)
conn.rollback()
finally:
put_conn(conn)
def run_scenario(client, steps, client_id, stand, user_email, app_version, scenario_run_id, scenario_name):
"""Главный исполнитель сценария. steps — список шагов из БД."""
total = len(steps)
t0 = time.time()
instance_map = {} # service_id → instanceUid (переиспользование внутри сценария)
for i, step in enumerate(steps):
step_num = i + 1
svc_id = step.get("service_id")
op_name = (step.get("operation") or "").strip()
symbolic_params = step.get("params", {})
try:
if not isinstance(svc_id, int) or svc_id <= 0:
raise ValueError(f"Invalid service_id: {svc_id!r}")
if not op_name:
raise ValueError("Empty operation name")
# 1. Операция
detail = get_service_detail(client, svc_id)
ops = detail.get("operations", [])
op = next((o for o in ops if o.get("operation") == op_name), None)
if not op:
raise ValueError(f"Operation '{op_name}' not found for service {svc_id}")
svc_op_id = op["svcOperationId"]
# 2. Параметры
tmpl_data = client.get(f"/instanceOperations/default/{svc_op_id}")
cfs_params = tmpl_data.get("svcOperation", {}).get("cfsParams", [])
resolved_params = _resolve_params(cfs_params, symbolic_params)
# 3. Инстанс
if op_name == "create":
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
descr = f"scenario {scenario_name} step {step_num}"
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:
raise RuntimeError(f"CREATE: no instanceUid in response: {json.dumps(resp)[:200]}")
instance_map[svc_id] = instance_uid
else:
instance_uid = instance_map.get(svc_id)
if not instance_uid:
raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first")
# 4. Запуск операции
if op_name == "create":
op_payload = {"instanceUid": instance_uid, "operation": op_name}
else:
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
op_resp = client.post("/instanceOperations", op_payload)
op_uid = op_resp.get("instanceOperationUid") or _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not op_uid:
raise RuntimeError(f"No opUid in response: {json.dumps(op_resp)[:200]}")
# Отправить параметры и запустить
from operations.terraform import send_params_terraform
send_params_terraform(client, op_uid, resolved_params)
client.post(f"/instanceOperations/{op_uid}/run")
# 5. Сохранить RUNNING в runs ДО поллинга
step_display = display_name if op_name == "create" else instance_uid
try:
save_run(client_id, stand, user_email,
svc_id, "", op_name, svc_op_id,
op_uid, instance_uid, step_display,
"RUNNING", 0, "",
resolved_params, [], app_version,
scenario_run_id, step_num)
except Exception as e:
print(f"[SCENARIO] save_run(RUNNING) failed for step {step_num}: {e}", flush=True)
# 6. Поллинг завершения
step_t0 = time.time()
deadline = step_t0 + 1800
step_status = "TIMEOUT"
step_error = ""
step_duration = 0
step_stages = []
op_data = {}
while time.time() < deadline:
try:
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,duration,stages,svc")
except Exception:
time.sleep(5)
continue
op_data = data.get("instanceOperation", {})
dt_finish = op_data.get("dtFinish")
if dt_finish and str(dt_finish).strip():
is_ok = op_data.get("isSuccessful")
err = op_data.get("errorLog") or ""
step_status = "OK" if is_ok else "FAIL"
step_error = str(err) if err else ""
step_duration = round(time.time() - step_t0, 1)
step_stages = op_data.get("stages", [])
break
time.sleep(5)
# 7. Сохранить финальный результат
svc_final = op_data.get("svc", "") if op_data else ""
try:
save_run(client_id, stand, user_email,
svc_id, svc_final, op_name, svc_op_id,
op_uid, instance_uid, step_display,
step_status, step_duration, step_error,
resolved_params, step_stages, app_version,
scenario_run_id, step_num)
except Exception as e:
print(f"[SCENARIO] save_run failed for step {step_num}: {e}", flush=True)
# Обновить scenario_runs
_save_scenario_run(scenario_run_id, "RUNNING" if step_num < total else step_status,
step_num, round(time.time() - t0, 1),
step_error if step_status != "OK" else None)
if step_status != "OK":
_save_scenario_run(scenario_run_id, step_status, step_num,
round(time.time() - t0, 1), step_error)
return
except Exception as e:
import traceback
err = f"Step {step_num}: {e}"
print(f"[SCENARIO] {err}", flush=True)
traceback.print_exc()
_save_scenario_run(scenario_run_id, "FAIL", step_num,
round(time.time() - t0, 1), err)
return
# Все шаги OK
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))