Files
app-autotest/site/operations/scenario.py
T
naeel 91e97f6f75 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
2026-07-31 08:55:06 +04:00

206 lines
8.2 KiB
Python

"""
Scenario runner — шаги из БД → API → результат.
Формат шага (новый):
{ "service_id": 1, "operation": "create", "params": {...}, "output": "d1" }
{ "service_id": 1, "operation": "modify", "instance_ref": "d1", "params": {...} }
{ "service_id": 1, "operation": "delete", "instance_uid": "UUID", "params": {} }
Старый формат (без output/instance_ref/instance_uid) — fallback на service_id.
Резолвинг: instance_uid > instance_ref > instance_map[service_id]
"""
import json
import time
import uuid
from operations.get_services import get_service_detail
from operations.executor import execute_operation
from operations.poll import poll_until_done
from db.pool import get_conn, put_conn
from db.save_run import save_run
AUTOTEST_PREFIX = "autotest-scenario-"
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 _update_bindings(scenario_run_id, bindings):
"""Сохранить output→instance_uid в scenario_runs.instance_bindings."""
conn = get_conn()
if not conn:
return
try:
cur = conn.cursor()
cur.execute("""
UPDATE scenario_runs SET instance_bindings = %s WHERE id = %s
""", (json.dumps(bindings), scenario_run_id))
conn.commit()
cur.close()
except Exception as e:
print(f"[SCENARIO] update_bindings error: {e}", flush=True)
conn.rollback()
finally:
put_conn(conn)
def _resolve_instance_uid(step, bindings, instance_map):
"""Резолвинг instance_uid по приоритету: явный uid > instance_ref > service_id."""
uid = (step.get("instance_uid") or "").strip()
if uid:
return uid
ref = (step.get("instance_ref") or "").strip()
if ref:
if ref in bindings:
return bindings[ref]
raise ValueError(f"instance_ref '{ref}' not found in bindings (step order issue?)")
# Fallback: старый формат по service_id
svc_id = step.get("service_id")
return instance_map.get(svc_id)
def run_scenario(client, steps, client_id, stand, user_email, app_version, scenario_run_id, scenario_name):
"""Главный исполнитель сценария. steps — список шагов из БД."""
total = len(steps)
t0 = time.time()
bindings = {} # output_name → instance_uid (новый формат)
instance_map = {} # service_id → instance_uid (fallback, старый формат)
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. Резолвинг instance_uid
is_create = (op_name == "create")
if is_create:
instance_uid = None
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
else:
instance_uid = _resolve_instance_uid(step, bindings, instance_map)
if not instance_uid:
raise ValueError(f"No instance for step {step_num} — need CREATE before non-create operations")
display_name = None
# 4. Запуск через executor
result = execute_operation(
client, svc_id, op_name, instance_uid, resolved_params,
svc_op_id=svc_op_id, display_name=display_name
)
if not result["ok"]:
raise RuntimeError(f"{op_name}: {result['error']} (step: {result['failed_step']})")
instance_uid = result["instance_uid"]
op_uid = result["op_uid"]
step_display = result["display_name"]
# Обновить карты
instance_map[svc_id] = instance_uid
output_name = (step.get("output") or "").strip()
if is_create and output_name:
bindings[output_name] = instance_uid
_update_bindings(scenario_run_id, bindings)
# 5. Сохранить RUNNING
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. Поллинг
poll_result = poll_until_done(client, op_uid)
step_status = poll_result["status"]
step_error = poll_result["error_log"]
step_duration = poll_result["duration"]
step_stages = poll_result["stages"]
svc_final = poll_result["svc"]
# 7. Сохранить финальный результат
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
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))