v1.1.50: scenario_definitions DB+CRUD, seed, runner from DB, 409 lock, UI definitions, drop config.yaml scenarios

This commit is contained in:
2026-07-30 15:06:05 +04:00
parent 67f6390509
commit 983b387708
8 changed files with 418 additions and 161 deletions
+28 -115
View File
@@ -1,22 +1,14 @@
"""
Scenario runner — символические коды → API → шаги → результат.
Scenario runner — шаги из БД → API → результат.
Формат сценария в config.yaml:
scenarios:
dummy_test:
steps:
- svc: dummy # символическое имя (из services: в config.yaml)
op: create
params:
durationMs: "5000"
- svc: dummy
op: delete
Формат шага:
{ "service_id": 1, "operation": "create", "params": { "durationMs": "5000" } }
Для каждого шага:
1. service имя → svc_id (из config.yaml services)
1. service_id уже в шаге
2. operation имя → svcOperationId (из GET /services/{id})
3. param коды → numeric IDs (из GET /instanceOperations/default/{opId})
4. create: новый инстанс; остальные: переиспользовать инстанс этого сервиса
4. create: новый инстанс; остальные: переиспользовать по service_id
5. Выполнить через API (как в api_test)
6. Сохранить результат в runs + scenario_runs
"""
@@ -25,7 +17,6 @@ import json
import time
import uuid
from runner import load_config
from operations.get_services import get_service_detail
from db.pool import get_conn, put_conn
from db.save_run import save_run
@@ -52,25 +43,8 @@ def _uid_from_location(loc):
return parts[-1] if parts[-1] else None
def load_scenarios():
"""Загрузить словарь сценариев из config.yaml."""
cfg = load_config() or {}
return cfg.get("scenarios", {})
def _resolve_service(config, svc_name):
"""Найти svc_id по символическому имени сервиса из config.yaml services."""
for s in config.get("services", []):
if s.get("name") == svc_name:
return s
raise ValueError(f"Service '{svc_name}' not found in config.yaml services")
def _resolve_params(cfs_params, symbolic_params):
"""Символические коды → {numeric_id: value}.
cfs_params — список из GET /instanceOperations/default/{opId}
symbolic_params — dict {"durationMs": "5000"}
"""
"""Символические коды → {numeric_id: value}."""
code_to_id = {p.get("svcOperationCfsParam", ""): p["svcOperationCfsParamId"] for p in cfs_params}
result = {}
for code, val in symbolic_params.items():
@@ -102,95 +76,39 @@ def _save_scenario_run(scenario_run_id, status, current_step=0, duration_sec=Non
put_conn(conn)
def _create_scenario_run(client_id, stand, user_email, scenario_name, total_steps, app_version):
"""Создать запись в scenario_runs, вернуть ID."""
conn = get_conn()
if not conn:
return None
rid = None
try:
cur = conn.cursor()
cur.execute("""
INSERT INTO scenario_runs (client_id, stand, user_email, scenario_name,
status, current_step, total_steps, app_version)
VALUES (%s, %s, %s, %s, 'RUNNING', 0, %s, %s)
RETURNING id
""", (client_id, stand, user_email, scenario_name, total_steps, app_version))
rid = cur.fetchone()[0]
conn.commit()
cur.close()
except Exception as e:
print(f"[SCENARIO] create_scenario_run error: {e}", flush=True)
conn.rollback()
finally:
put_conn(conn)
return rid
def _update_scenario_total(scenario_run_id, total_steps):
"""Обновить total_steps после того как стало известно точное число шагов."""
conn = get_conn()
if not conn:
return
try:
cur = conn.cursor()
cur.execute("UPDATE scenario_runs SET total_steps = %s WHERE id = %s", (total_steps, scenario_run_id))
conn.commit()
cur.close()
except Exception as e:
print(f"[SCENARIO] update_total error: {e}", flush=True)
conn.rollback()
finally:
put_conn(conn)
def run_scenario(client, scenario_name, client_id, stand, user_email, app_version, scenario_run_id):
"""Главный исполнитель сценария. Вызывается в фоновом потоке.
scenario_run_id — уже созданная запись (из api_scenario_run)."""
config = load_config() or {}
scenarios = config.get("scenarios", {})
scenario = scenarios.get(scenario_name)
if not scenario:
_save_scenario_run(scenario_run_id, "FAIL", 0, 0, f"Scenario '{scenario_name}' not found")
return
steps = scenario.get("steps", [])
def run_scenario(client, steps, client_id, stand, user_email, app_version, scenario_run_id, scenario_name):
"""Главный исполнитель сценария. steps — список шагов из БД."""
total = len(steps)
if not steps:
_save_scenario_run(scenario_run_id, "FAIL", 0, 0, "No steps")
return
# Обновить total_steps (стало известно только сейчас)
_update_scenario_total(scenario_run_id, total)
t0 = time.time()
instance_map = {} # svc_name → instanceUid (переиспользование внутри сценария)
instance_map = {} # service_id → instanceUid (переиспользование внутри сценария)
for i, step in enumerate(steps):
step_num = i + 1
svc_name = step.get("svc", "")
op_name = step.get("op", "")
svc_id = step.get("service_id")
op_name = (step.get("operation") or "").strip()
symbolic_params = step.get("params", {})
try:
# 1. Сервис
svc_info = _resolve_service(config, svc_name)
svc_id = svc_info["service_id"]
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")
# 2. Операция
# 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_name}")
raise ValueError(f"Operation '{op_name}' not found for service {svc_id}")
svc_op_id = op["svcOperationId"]
# 3. Параметры
# 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)
# 4. Инстанс
# 3. Инстанс
if op_name == "create":
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
descr = f"scenario {scenario_name} step {step_num}"
@@ -199,20 +117,16 @@ def run_scenario(client, scenario_name, client_id, stand, user_email, app_versio
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_name] = instance_uid
instance_map[svc_id] = instance_uid
else:
instance_uid = instance_map.get(svc_name)
instance_uid = instance_map.get(svc_id)
if not instance_uid:
raise RuntimeError(f"No instance for service '{svc_name}' — need CREATE first")
raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first")
# 5. Запуск операции
# 4. Запуск операции
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 op_resp.get("instanceOperation", {}).get("instanceOperationUid")
if not op_uid:
op_uid = _find_uid(op_resp)
if not op_uid:
op_uid = _uid_from_location(op_resp.get("_location", ""))
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]}")
@@ -221,7 +135,7 @@ def run_scenario(client, scenario_name, client_id, stand, user_email, app_versio
_send_params_terraform(client, op_uid, resolved_params)
client.post(f"/instanceOperations/{op_uid}/run")
# 5b. Сохранить RUNNING в runs ДО поллинга (шаг не потеряется при ошибке)
# 5. Сохранить RUNNING в runs ДО поллинга
step_display = display_name if op_name == "create" else instance_uid
try:
save_run(client_id, stand, user_email,
@@ -240,7 +154,7 @@ def run_scenario(client, scenario_name, client_id, stand, user_email, app_versio
step_error = ""
step_duration = 0
step_stages = []
op_data = {} # инициализировать на случай TIMEOUT
op_data = {}
while time.time() < deadline:
try:
@@ -260,8 +174,8 @@ def run_scenario(client, scenario_name, client_id, stand, user_email, app_versio
break
time.sleep(5)
# 7. Сохранить финальный результат шага в runs
svc_final = op_data.get("svc", svc_name) if op_data else svc_name
# 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,
@@ -278,7 +192,6 @@ def run_scenario(client, scenario_name, client_id, stand, user_email, app_versio
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