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:
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Общие утилиты: извлечение UUID из ответов API Nubes.
|
||||||
|
|
||||||
|
Используется: api_test.py, scenario.py, executor.py.
|
||||||
|
Заменяет разрозненные реализации _find_uid / _uid_from_location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_uid(resp):
|
||||||
|
"""Извлечь instanceUid или instanceOperationUid из ответа API.
|
||||||
|
|
||||||
|
Ищет в нескольких местах:
|
||||||
|
1. На верхнем уровне: instanceOperationUid → instanceUid → uid → Uid
|
||||||
|
2. Во вложенных dict-значениях: {key: {instanceOperationUid: ..., instanceUid: ...}}
|
||||||
|
"""
|
||||||
|
if not isinstance(resp, dict):
|
||||||
|
return None
|
||||||
|
# Верхний уровень — прямые ключи (api_test.py-стиль)
|
||||||
|
for key in ("instanceOperationUid", "instanceUid", "uid", "Uid"):
|
||||||
|
v = resp.get(key)
|
||||||
|
if isinstance(v, str) and v:
|
||||||
|
return v
|
||||||
|
# Вложенные dict-значения (scenario.py-стиль)
|
||||||
|
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):
|
||||||
|
"""Извлечь UUID из Location-заголовка.
|
||||||
|
|
||||||
|
Location бывает: "./UUID", "/api/v1/svc/instances/UUID", "/api/v1/svc/instanceOperations/UUID"
|
||||||
|
Берём последний сегмент после split("/").
|
||||||
|
"""
|
||||||
|
if not loc:
|
||||||
|
return None
|
||||||
|
parts = str(loc).rstrip("/").split("/")
|
||||||
|
uid = parts[-1]
|
||||||
|
# UUID — 36 символов с 4 дефисами (уже проверено в http_client.py)
|
||||||
|
if uid and uid != "." and len(uid) >= 32:
|
||||||
|
return uid
|
||||||
|
return None
|
||||||
+1
-1
@@ -19,7 +19,7 @@ from routes.api_scenario_run import bp_run as api_scenario_run_bp
|
|||||||
from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
||||||
|
|
||||||
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
||||||
VERSION = "1.1.57"
|
VERSION = "1.2.0"
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
|
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc")
|
||||||
|
|||||||
@@ -159,6 +159,23 @@ def init_db():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
print("[DB] Schema initialized", flush=True)
|
print("[DB] Schema initialized", flush=True)
|
||||||
|
|
||||||
|
# Startup cleanup: зависшие RUNNING сценарии старше 1 часа → TIMEOUT
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE scenario_runs
|
||||||
|
SET status = 'TIMEOUT', error_log = 'worker restart'
|
||||||
|
WHERE status = 'RUNNING'
|
||||||
|
AND created_at < NOW() - INTERVAL '1 hour'
|
||||||
|
""")
|
||||||
|
if cur.rowcount:
|
||||||
|
print(f"[DB] Cleaned up {cur.rowcount} stuck scenario_runs", flush=True)
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DB] Cleanup error: {e}", flush=True)
|
||||||
|
conn.rollback()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DB] Init error: {e}", flush=True)
|
print(f"[DB] Init error: {e}", flush=True)
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Единый executor операции Nubes.
|
||||||
|
|
||||||
|
Делает всё до /run включительно. НЕ поллит.
|
||||||
|
Используется api_test.py (ручной) и scenario.py (сценарный).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from api.utils import find_uid, uid_from_location
|
||||||
|
from operations.terraform import send_params_terraform
|
||||||
|
|
||||||
|
|
||||||
|
def execute_operation(client, service_id, operation, instance_uid, params,
|
||||||
|
svc_op_id=None, display_name=None):
|
||||||
|
"""Запустить операцию и вернуть результат.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: HttpClient
|
||||||
|
service_id: int — ID сервиса
|
||||||
|
operation: str — create/delete/modify/suspend/resume/redeploy
|
||||||
|
instance_uid: str|None — UUID существующего инстанса (None для create)
|
||||||
|
params: dict — {numeric_param_id: value}
|
||||||
|
svc_op_id: int|None — svcOperationId (игнорируется для create)
|
||||||
|
display_name: str|None — displayName (только для create)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict {ok, error, failed_step, instance_uid, op_uid, display_name}
|
||||||
|
ok=True при успехе, ok=False при ошибке с failed_step.
|
||||||
|
"""
|
||||||
|
is_create = (operation == "create")
|
||||||
|
|
||||||
|
# --- Шаг 1: POST /instances (только create) ---
|
||||||
|
if is_create:
|
||||||
|
if not display_name:
|
||||||
|
display_name = f"autotest-{service_id}"
|
||||||
|
descr = f"created by autotest"
|
||||||
|
payload = {"serviceId": service_id, "displayName": display_name, "descr": descr}
|
||||||
|
try:
|
||||||
|
resp = client.post("/instances", payload)
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e), "failed_step": "instances",
|
||||||
|
"instance_uid": None, "op_uid": None, "display_name": display_name}
|
||||||
|
instance_uid = resp.get("instanceUid") or find_uid(resp) or uid_from_location(resp.get("_location", ""))
|
||||||
|
if not instance_uid:
|
||||||
|
return {"ok": False, "error": "No instanceUid in response", "failed_step": "instances",
|
||||||
|
"instance_uid": None, "op_uid": None, "display_name": display_name}
|
||||||
|
|
||||||
|
# --- Шаг 2: POST /instanceOperations ---
|
||||||
|
if is_create:
|
||||||
|
op_payload = {"instanceUid": instance_uid, "operation": operation}
|
||||||
|
else:
|
||||||
|
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": operation}
|
||||||
|
try:
|
||||||
|
op_resp = client.post("/instanceOperations", op_payload)
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e), "failed_step": "instanceOperations",
|
||||||
|
"instance_uid": instance_uid, "op_uid": None, "display_name": display_name}
|
||||||
|
op_uid = op_resp.get("instanceOperationUid") or find_uid(op_resp) or uid_from_location(op_resp.get("_location", ""))
|
||||||
|
if not op_uid:
|
||||||
|
return {"ok": False, "error": "No opUid in response", "failed_step": "instanceOperations",
|
||||||
|
"instance_uid": instance_uid, "op_uid": None, "display_name": display_name}
|
||||||
|
|
||||||
|
# --- Шаг 3-6: Параметры + run ---
|
||||||
|
try:
|
||||||
|
send_params_terraform(client, op_uid, params)
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e), "failed_step": "params",
|
||||||
|
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}
|
||||||
|
try:
|
||||||
|
client.post(f"/instanceOperations/{op_uid}/run")
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e), "failed_step": "run",
|
||||||
|
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}
|
||||||
|
|
||||||
|
return {"ok": True, "error": None, "failed_step": None,
|
||||||
|
"instance_uid": instance_uid, "op_uid": op_uid, "display_name": display_name}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Общий цикл поллинга операции Nubes.
|
||||||
|
|
||||||
|
Используется:
|
||||||
|
- _finish_op (api_test.py) — асинхронно в threading.Thread
|
||||||
|
- run_scenario (scenario.py) — синхронно в while
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def poll_until_done(client, op_uid, timeout=1800):
|
||||||
|
"""Поллинг операции до завершения или таймаута.
|
||||||
|
|
||||||
|
Возвращает dict:
|
||||||
|
{status, is_successful, error_log, stages, duration, svc}
|
||||||
|
status ∈ {OK, FAIL, TIMEOUT}
|
||||||
|
"""
|
||||||
|
t0 = time.time()
|
||||||
|
deadline = t0 + timeout
|
||||||
|
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.get("instanceOperation", {})
|
||||||
|
dt_finish = op.get("dtFinish")
|
||||||
|
if dt_finish and str(dt_finish).strip():
|
||||||
|
is_ok = op.get("isSuccessful")
|
||||||
|
return {
|
||||||
|
"status": "OK" if is_ok else "FAIL",
|
||||||
|
"is_successful": is_ok,
|
||||||
|
"error_log": op.get("errorLog") or "",
|
||||||
|
"stages": op.get("stages", []),
|
||||||
|
"duration": round(time.time() - t0, 1),
|
||||||
|
"svc": op.get("svc", ""),
|
||||||
|
}
|
||||||
|
time.sleep(5)
|
||||||
|
# Таймаут
|
||||||
|
return {
|
||||||
|
"status": "TIMEOUT",
|
||||||
|
"is_successful": False,
|
||||||
|
"error_log": f"TIMEOUT after {timeout}s",
|
||||||
|
"stages": [],
|
||||||
|
"duration": round(time.time() - t0, 1),
|
||||||
|
"svc": "",
|
||||||
|
}
|
||||||
+79
-86
@@ -1,16 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Scenario runner — шаги из БД → API → результат.
|
Scenario runner — шаги из БД → API → результат.
|
||||||
|
|
||||||
Формат шага:
|
Формат шага (новый):
|
||||||
{ "service_id": 1, "operation": "create", "params": { "durationMs": "5000" } }
|
{ "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.
|
||||||
1. service_id уже в шаге
|
|
||||||
2. operation имя → svcOperationId (из GET /services/{id})
|
Резолвинг: instance_uid > instance_ref > instance_map[service_id]
|
||||||
3. param коды → numeric IDs (из GET /instanceOperations/default/{opId})
|
|
||||||
4. create: новый инстанс; остальные: переиспользовать по service_id
|
|
||||||
5. Выполнить через API (как в api_test)
|
|
||||||
6. Сохранить результат в runs + scenario_runs
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -18,31 +16,14 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from operations.get_services import get_service_detail
|
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.pool import get_conn, put_conn
|
||||||
from db.save_run import save_run
|
from db.save_run import save_run
|
||||||
|
|
||||||
AUTOTEST_PREFIX = "autotest-scenario-"
|
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):
|
def _resolve_params(cfs_params, symbolic_params):
|
||||||
"""Символические коды → {numeric_id: value}."""
|
"""Символические коды → {numeric_id: value}."""
|
||||||
code_to_id = {p.get("svcOperationCfsParam", ""): p["svcOperationCfsParamId"] for p in cfs_params}
|
code_to_id = {p.get("svcOperationCfsParam", ""): p["svcOperationCfsParamId"] for p in cfs_params}
|
||||||
@@ -76,12 +57,46 @@ def _save_scenario_run(scenario_run_id, status, current_step=0, duration_sec=Non
|
|||||||
put_conn(conn)
|
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):
|
def run_scenario(client, steps, client_id, stand, user_email, app_version, scenario_run_id, scenario_name):
|
||||||
"""Главный исполнитель сценария. steps — список шагов из БД."""
|
"""Главный исполнитель сценария. steps — список шагов из БД."""
|
||||||
total = len(steps)
|
total = len(steps)
|
||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
instance_map = {} # service_id → instanceUid (переиспользование внутри сценария)
|
bindings = {} # output_name → instance_uid (новый формат)
|
||||||
|
instance_map = {} # service_id → instance_uid (fallback, старый формат)
|
||||||
|
|
||||||
for i, step in enumerate(steps):
|
for i, step in enumerate(steps):
|
||||||
step_num = i + 1
|
step_num = i + 1
|
||||||
@@ -108,38 +123,37 @@ def run_scenario(client, steps, client_id, stand, user_email, app_version, scena
|
|||||||
cfs_params = tmpl_data.get("svcOperation", {}).get("cfsParams", [])
|
cfs_params = tmpl_data.get("svcOperation", {}).get("cfsParams", [])
|
||||||
resolved_params = _resolve_params(cfs_params, symbolic_params)
|
resolved_params = _resolve_params(cfs_params, symbolic_params)
|
||||||
|
|
||||||
# 3. Инстанс
|
# 3. Резолвинг instance_uid
|
||||||
if op_name == "create":
|
is_create = (op_name == "create")
|
||||||
|
if is_create:
|
||||||
|
instance_uid = None
|
||||||
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
|
display_name = f"{AUTOTEST_PREFIX}{scenario_name}-{uuid.uuid4().hex[:6]}"
|
||||||
descr = f"scenario {scenario_name} step {step_num}"
|
else:
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
instance_uid = _resolve_instance_uid(step, bindings, instance_map)
|
||||||
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:
|
if not instance_uid:
|
||||||
raise RuntimeError(f"CREATE: no instanceUid in response: {json.dumps(resp)[:200]}")
|
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
|
instance_map[svc_id] = instance_uid
|
||||||
else:
|
output_name = (step.get("output") or "").strip()
|
||||||
instance_uid = instance_map.get(svc_id)
|
if is_create and output_name:
|
||||||
if not instance_uid:
|
bindings[output_name] = instance_uid
|
||||||
raise RuntimeError(f"No instance for service_id {svc_id} — need CREATE first")
|
_update_bindings(scenario_run_id, bindings)
|
||||||
|
|
||||||
# 4. Запуск операции
|
# 5. Сохранить RUNNING
|
||||||
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:
|
try:
|
||||||
save_run(client_id, stand, user_email,
|
save_run(client_id, stand, user_email,
|
||||||
svc_id, "", op_name, svc_op_id,
|
svc_id, "", op_name, svc_op_id,
|
||||||
@@ -150,35 +164,15 @@ def run_scenario(client, steps, client_id, stand, user_email, app_version, scena
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SCENARIO] save_run(RUNNING) failed for step {step_num}: {e}", flush=True)
|
print(f"[SCENARIO] save_run(RUNNING) failed for step {step_num}: {e}", flush=True)
|
||||||
|
|
||||||
# 6. Поллинг завершения
|
# 6. Поллинг
|
||||||
step_t0 = time.time()
|
poll_result = poll_until_done(client, op_uid)
|
||||||
deadline = step_t0 + 1800
|
step_status = poll_result["status"]
|
||||||
step_status = "TIMEOUT"
|
step_error = poll_result["error_log"]
|
||||||
step_error = ""
|
step_duration = poll_result["duration"]
|
||||||
step_duration = 0
|
step_stages = poll_result["stages"]
|
||||||
step_stages = []
|
svc_final = poll_result["svc"]
|
||||||
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. Сохранить финальный результат
|
# 7. Сохранить финальный результат
|
||||||
svc_final = op_data.get("svc", "") if op_data else ""
|
|
||||||
try:
|
try:
|
||||||
save_run(client_id, stand, user_email,
|
save_run(client_id, stand, user_email,
|
||||||
svc_id, svc_final, op_name, svc_op_id,
|
svc_id, svc_final, op_name, svc_op_id,
|
||||||
@@ -208,5 +202,4 @@ def run_scenario(client, steps, client_id, stand, user_email, app_version, scena
|
|||||||
round(time.time() - t0, 1), err)
|
round(time.time() - t0, 1), err)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Все шаги OK
|
|
||||||
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))
|
_save_scenario_run(scenario_run_id, "OK", total, round(time.time() - t0, 1))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ def _validate_steps(steps):
|
|||||||
"""Проверить структуру шагов сценария. Возвращает None или str с ошибкой."""
|
"""Проверить структуру шагов сценария. Возвращает None или str с ошибкой."""
|
||||||
if not isinstance(steps, list) or len(steps) == 0:
|
if not isinstance(steps, list) or len(steps) == 0:
|
||||||
return "Шаги: непустой массив"
|
return "Шаги: непустой массив"
|
||||||
|
outputs = {} # output_name → step_index (для проверки ссылок и уникальности)
|
||||||
for i, s in enumerate(steps):
|
for i, s in enumerate(steps):
|
||||||
if not isinstance(s, dict):
|
if not isinstance(s, dict):
|
||||||
return f"Шаг {i+1}: должен быть объектом"
|
return f"Шаг {i+1}: должен быть объектом"
|
||||||
@@ -33,6 +34,30 @@ def _validate_steps(steps):
|
|||||||
return f"Шаг {i+1}: operation обязателен"
|
return f"Шаг {i+1}: operation обязателен"
|
||||||
if not isinstance(s.get("params", {}), dict):
|
if not isinstance(s.get("params", {}), dict):
|
||||||
return f"Шаг {i+1}: params — объект"
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+51
-109
@@ -31,10 +31,13 @@ import fcntl
|
|||||||
|
|
||||||
from api.http_client import HttpClient, detect_endpoint, stand_name
|
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.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_services import get_services, get_service_detail
|
||||||
from operations.get_instances import get_instances
|
from operations.get_instances import get_instances
|
||||||
from operations.get_params import get_params_with_current_values, _normalize_value_list
|
from operations.get_params import get_params_with_current_values, _normalize_value_list
|
||||||
from operations.terraform import send_params_terraform, redact_params
|
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 add as tracker_add, list_all as tracker_list
|
||||||
from operations.tracker import remove as tracker_remove
|
from operations.tracker import remove as tracker_remove
|
||||||
|
|
||||||
@@ -190,21 +193,17 @@ def api_test():
|
|||||||
if not display_name:
|
if not display_name:
|
||||||
display_name = f"autotest-{svc_id}"
|
display_name = f"autotest-{svc_id}"
|
||||||
display_name = _unique_display_name(client, display_name)
|
display_name = _unique_display_name(client, display_name)
|
||||||
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
result = execute_operation(
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
client, svc_id, op_name, None, params,
|
||||||
resp = client.post("/instances", payload)
|
svc_op_id=svc_op_id, display_name=display_name
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
)
|
||||||
if not instance_uid:
|
if not result["ok"]:
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
|
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:
|
try:
|
||||||
tracker_add(get_client_id(), get_stand(), instance_uid, svc_id, display_name)
|
tracker_add(get_client_id(), get_stand(), instance_uid, svc_id, display_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -212,12 +211,7 @@ def api_test():
|
|||||||
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
|
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
|
||||||
traceback.print_exc()
|
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", "")
|
user_email = get_token_info().get("email", "")
|
||||||
app_version = current_app.config.get("VERSION", "")
|
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()
|
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})
|
return jsonify({"status": "OK", "opUid": "cmdb-"+instance_uid[:8], "instanceUid": instance_uid, "displayName": display_name})
|
||||||
|
|
||||||
if op_name == "redeploy":
|
if op_name == "redeploy":
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
result = execute_operation(
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
client, svc_id, op_name, instance_uid, {},
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
svc_op_id=svc_op_id
|
||||||
if not op_uid:
|
)
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
if not result["ok"]:
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
return jsonify({"status": "FAIL", "error": result["error"]}), 500
|
||||||
|
op_uid = result["op_uid"]
|
||||||
else:
|
else:
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
result = execute_operation(
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
client, svc_id, op_name, instance_uid, params,
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
svc_op_id=svc_op_id
|
||||||
if not op_uid:
|
)
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
if not result["ok"]:
|
||||||
send_params_terraform(client, op_uid, params)
|
return jsonify({"status": "FAIL", "error": result["error"]}), 500
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
op_uid = result["op_uid"]
|
||||||
|
|
||||||
# фоном ждать завершения
|
# фоном ждать завершения
|
||||||
is_delete = (op_name == "delete")
|
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=""):
|
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 и сохранить результат."""
|
"""Фоном ждать dtFinish и сохранить результат."""
|
||||||
import time
|
import time
|
||||||
_cleanup_op_results() # очистить старые записи перед добавлением новой
|
_cleanup_op_results()
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
deadline = t0 + 1800
|
|
||||||
while time.time() < deadline:
|
# Поллинг через общий модуль
|
||||||
try:
|
poll_result = poll_until_done(client, op_uid)
|
||||||
try:
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages,svc")
|
# Обновить _op_results для UI
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
# Обновляем stages по мере появления
|
|
||||||
_op_results[op_uid] = {
|
_op_results[op_uid] = {
|
||||||
"status": "RUNNING",
|
"status": poll_result["status"],
|
||||||
"displayName": display_name,
|
"displayName": display_name,
|
||||||
"stages": op.get("stages", []),
|
"error": poll_result["error_log"],
|
||||||
"duration": round(time.time() - t0, 1),
|
"stages": poll_result["stages"],
|
||||||
|
"duration": poll_result["duration"],
|
||||||
"_ts": time.time(),
|
"_ts": time.time(),
|
||||||
}
|
}
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
# tracker_remove при успешном delete
|
||||||
err = op.get("errorLog") or ""
|
if poll_result["status"] == "OK" and is_delete:
|
||||||
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
|
try:
|
||||||
# tracker_add для create уже вызван синхронно в api_test()
|
|
||||||
if is_ok and is_delete:
|
|
||||||
tracker_remove(client_id, stand, instance_uid)
|
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)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
pass
|
||||||
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 в БД
|
|
||||||
try:
|
try:
|
||||||
from db.save_run import save_run
|
from db.save_run import save_run
|
||||||
saved_params = redact_params(params or {})
|
saved_params = redact_params(params or {})
|
||||||
save_run(client_id, stand, user_email,
|
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,
|
op_uid, instance_uid, display_name,
|
||||||
"TIMEOUT", round(time.time() - t0, 1),
|
poll_result["status"],
|
||||||
"TIMEOUT after 1800s", saved_params, [], app_version)
|
poll_result["duration"],
|
||||||
|
poll_result["error_log"],
|
||||||
|
saved_params, poll_result["stages"], app_version)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
print(f"[DB] TIMEOUT save_run FAILED: {e}", flush=True)
|
print(f"[DB] save_run FAILED: {e}", flush=True)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
@@ -459,21 +419,3 @@ def api_history():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return jsonify({"error": str(e)}), 500
|
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
|
|
||||||
|
|||||||
@@ -78,80 +78,7 @@ function showParams(opId,opName){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderParamRow(p,allInst){
|
// renderParamRow, renderMapFixedRow, collectParams — теперь в params-render.js (общий модуль)
|
||||||
const req=p.isRequired;
|
|
||||||
const hasDfl=p.defaultValue!==null&&p.defaultValue!==undefined&&p.defaultValue!=='';
|
|
||||||
const labelCls=req?(hasDfl?'req':'req-nodfl'):'';
|
|
||||||
const dfl=hasDfl?p.defaultValue:(req?'':'');
|
|
||||||
const vl=p.valueList;
|
|
||||||
const isMap=(p.dataType||'').startsWith('map');
|
|
||||||
let input;
|
|
||||||
if(vl&&Array.isArray(vl)){
|
|
||||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="${_esc(p.dataType)}">${vl.map(v=>`<option value="${_esc(v)}" ${v==dfl?'selected':''}>${_esc(v)}</option>`).join('')}</select>`;
|
|
||||||
}else if(p.refSvcId){
|
|
||||||
const refInsts=allInst.filter(i=>i.serviceId===p.refSvcId&&i.explainedStatus!=='deleted');
|
|
||||||
let opts=refInsts.map(i=>`<option value="${_esc(i.instanceUid)}" ${i.instanceUid===dfl?'selected':''}>${_esc(i.displayName)}</option>`).join('');
|
|
||||||
if(!dfl) opts='<option value="">— выбрать —</option>'+opts;
|
|
||||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="ref">${opts}</select>`;
|
|
||||||
}else if(p.dataType==='boolean'){
|
|
||||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="boolean"><option value="true" ${dfl==='true'||dfl===true?'selected':''}>true</option><option value="false" ${dfl==='false'||dfl===false?'selected':''}>false</option></select>`;
|
|
||||||
}else if(p.dataDescriptor&&isMap){
|
|
||||||
return renderMapFixedRow(p,dfl);
|
|
||||||
}else{
|
|
||||||
const dt=isMap?'map':'';
|
|
||||||
input=`<input type="text" name="p_${p.svcOperationCfsParamId}" value="${_esc(dfl)}" data-type="${dt}" placeholder="${req&&!hasDfl?'обязательно':''}"${isMap?' onblur="validateJson(this)"':''}>`;
|
|
||||||
if(isMap) input+=`<span class="json-err" style="display:none;color:var(--destructive);font-size:10px;margin-left:4px;"></span>`;
|
|
||||||
}
|
|
||||||
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label>${input}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMapFixedRow(p,dfl){
|
|
||||||
const dd=p.dataDescriptor;
|
|
||||||
let subHtml='';
|
|
||||||
let dflObj={};
|
|
||||||
try{dflObj=JSON.parse(dfl||'{}');}catch(e){}
|
|
||||||
Object.keys(dd).forEach(subKey=>{
|
|
||||||
const sub=dd[subKey];
|
|
||||||
const subDfl=dflObj[subKey]!==undefined?dflObj[subKey]:(sub.defaultValue||'');
|
|
||||||
const subVl=sub.valueList;
|
|
||||||
const subReq=sub.isRequired;
|
|
||||||
const subNoDfl=subReq&&!subDfl&&subDfl!==0&&subDfl!==false;
|
|
||||||
const subLblCls=subNoDfl?'req-nodfl':'';
|
|
||||||
let si;
|
|
||||||
if(subVl&&Array.isArray(subVl)){
|
|
||||||
si=`<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild">${subVl.map(v=>`<option value="${_esc(v)}" ${v==subDfl?'selected':''}>${_esc(v)}</option>`).join('')}</select>`;
|
|
||||||
}else if(sub.dataType==='boolean'){
|
|
||||||
si=`<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild"><option value="true" ${subDfl==='true'||subDfl===true?'selected':''}>true</option><option value="false" ${subDfl==='false'||subDfl===false?'selected':''}>false</option></select>`;
|
|
||||||
}else{
|
|
||||||
si=`<input type="text" name="p_${p.svcOperationCfsParamId}.${subKey}" value="${_esc(subDfl)}" data-type="mapchild">`;
|
|
||||||
}
|
|
||||||
subHtml+=`<div class="param-row"><label class="${subLblCls}">${_esc(subKey)}</label>${si}</div>`;
|
|
||||||
});
|
|
||||||
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label></div><div style="border-left:2px solid var(--brand-gray);margin-left:8px;padding:0 0 4px 8px;">${subHtml}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectParams(){
|
|
||||||
const pp={};
|
|
||||||
const nested={};
|
|
||||||
document.querySelectorAll('#params-form [name^="p_"]').forEach(el=>{
|
|
||||||
let v=el.value;
|
|
||||||
const dt=el.dataset.type||'';
|
|
||||||
if(dt==='mapchild'){
|
|
||||||
const name=el.name.replace('p_','');
|
|
||||||
const dot=name.indexOf('.');
|
|
||||||
const parentKey=name.substring(0,dot);
|
|
||||||
const subKey=name.substring(dot+1);
|
|
||||||
if(!nested[parentKey]) nested[parentKey]={};
|
|
||||||
nested[parentKey][subKey]=v;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(dt==='array'||dt.startsWith('array')) v=JSON.stringify([v]);
|
|
||||||
if(dt==='map'||dt==='map-fixed') v=v||'{}';
|
|
||||||
pp[el.name.replace('p_','')]=v;
|
|
||||||
});
|
|
||||||
for(const pk in nested) pp[pk]=JSON.stringify(nested[pk]);
|
|
||||||
return pp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
|
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
|
||||||
busy=false;
|
busy=false;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// params-render.js — общий рендер параметров операций
|
||||||
|
// Используется: operations.js (ручной режим), scenario-form.js (редактор сценариев)
|
||||||
|
// Зависимости: _esc() из utils.js, validateJson() из utils.js
|
||||||
|
|
||||||
|
function renderParamRow(p, allInst) {
|
||||||
|
const req = p.isRequired;
|
||||||
|
const hasDfl = p.defaultValue !== null && p.defaultValue !== undefined && p.defaultValue !== '';
|
||||||
|
const labelCls = req ? (hasDfl ? 'req' : 'req-nodfl') : '';
|
||||||
|
const dfl = hasDfl ? p.defaultValue : (req ? '' : '');
|
||||||
|
const vl = p.valueList;
|
||||||
|
const isMap = (p.dataType || '').startsWith('map');
|
||||||
|
let input;
|
||||||
|
if (vl && Array.isArray(vl)) {
|
||||||
|
input = `<select name="p_${p.svcOperationCfsParamId}" data-type="${_esc(p.dataType)}">${vl.map(v => `<option value="${_esc(v)}" ${v == dfl ? 'selected' : ''}>${_esc(v)}</option>`).join('')}</select>`;
|
||||||
|
} else if (p.refSvcId) {
|
||||||
|
const refInsts = allInst.filter(i => i.serviceId === p.refSvcId && i.explainedStatus !== 'deleted');
|
||||||
|
let opts = refInsts.map(i => `<option value="${_esc(i.instanceUid)}" ${i.instanceUid === dfl ? 'selected' : ''}>${_esc(i.displayName)}</option>`).join('');
|
||||||
|
if (!dfl) opts = '<option value="">— выбрать —</option>' + opts;
|
||||||
|
input = `<select name="p_${p.svcOperationCfsParamId}" data-type="ref">${opts}</select>`;
|
||||||
|
} else if (p.dataType === 'boolean') {
|
||||||
|
input = `<select name="p_${p.svcOperationCfsParamId}" data-type="boolean"><option value="true" ${dfl === 'true' || dfl === true ? 'selected' : ''}>true</option><option value="false" ${dfl === 'false' || dfl === false ? 'selected' : ''}>false</option></select>`;
|
||||||
|
} else if (p.dataDescriptor && isMap) {
|
||||||
|
return renderMapFixedRow(p, dfl);
|
||||||
|
} else {
|
||||||
|
const dt = isMap ? 'map' : '';
|
||||||
|
input = `<input type="text" name="p_${p.svcOperationCfsParamId}" value="${_esc(dfl)}" data-type="${dt}" placeholder="${req && !hasDfl ? 'обязательно' : ''}"${isMap ? ' onblur="validateJson(this)"' : ''}>`;
|
||||||
|
if (isMap) input += `<span class="json-err" style="display:none;color:var(--destructive);font-size:10px;margin-left:4px;"></span>`;
|
||||||
|
}
|
||||||
|
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label>${input}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMapFixedRow(p, dfl) {
|
||||||
|
const dd = p.dataDescriptor;
|
||||||
|
let subHtml = '';
|
||||||
|
let dflObj = {};
|
||||||
|
try { dflObj = JSON.parse(dfl || '{}'); } catch (e) { }
|
||||||
|
Object.keys(dd).forEach(subKey => {
|
||||||
|
const sub = dd[subKey];
|
||||||
|
const subDfl = dflObj[subKey] !== undefined ? dflObj[subKey] : (sub.defaultValue || '');
|
||||||
|
const subVl = sub.valueList;
|
||||||
|
const subReq = sub.isRequired;
|
||||||
|
const subNoDfl = subReq && !subDfl && subDfl !== 0 && subDfl !== false;
|
||||||
|
const subLblCls = subNoDfl ? 'req-nodfl' : '';
|
||||||
|
let si;
|
||||||
|
if (subVl && Array.isArray(subVl)) {
|
||||||
|
si = `<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild">${subVl.map(v => `<option value="${_esc(v)}" ${v == subDfl ? 'selected' : ''}>${_esc(v)}</option>`).join('')}</select>`;
|
||||||
|
} else if (sub.dataType === 'boolean') {
|
||||||
|
si = `<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild"><option value="true" ${subDfl === 'true' || subDfl === true ? 'selected' : ''}>true</option><option value="false" ${subDfl === 'false' || subDfl === false ? 'selected' : ''}>false</option></select>`;
|
||||||
|
} else {
|
||||||
|
si = `<input type="text" name="p_${p.svcOperationCfsParamId}.${subKey}" value="${_esc(subDfl)}" data-type="mapchild">`;
|
||||||
|
}
|
||||||
|
subHtml += `<div class="param-row"><label class="${subLblCls}">${_esc(subKey)}</label>${si}</div>`;
|
||||||
|
});
|
||||||
|
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label></div><div style="border-left:2px solid var(--brand-gray);margin-left:8px;padding:0 0 4px 8px;">${subHtml}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectParams(containerSelector) {
|
||||||
|
containerSelector = containerSelector || '#params-form';
|
||||||
|
const pp = {};
|
||||||
|
const nested = {};
|
||||||
|
const container = document.querySelector(containerSelector);
|
||||||
|
if (!container) return pp;
|
||||||
|
container.querySelectorAll('[name^="p_"]').forEach(el => {
|
||||||
|
let v = el.value;
|
||||||
|
const dt = el.dataset.type || '';
|
||||||
|
if (dt === 'mapchild') {
|
||||||
|
const name = el.name.replace('p_', '');
|
||||||
|
const dot = name.indexOf('.');
|
||||||
|
const parentKey = name.substring(0, dot);
|
||||||
|
const subKey = name.substring(dot + 1);
|
||||||
|
if (!nested[parentKey]) nested[parentKey] = {};
|
||||||
|
nested[parentKey][subKey] = v;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dt === 'array' || dt.startsWith('array')) v = JSON.stringify([v]);
|
||||||
|
if (dt === 'map' || dt === 'map-fixed') v = v || '{}';
|
||||||
|
pp[el.name.replace('p_', '')] = v;
|
||||||
|
});
|
||||||
|
for (const pk in nested) pp[pk] = JSON.stringify(nested[pk]);
|
||||||
|
return pp;
|
||||||
|
}
|
||||||
@@ -149,6 +149,7 @@ window.APP = {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/params-render.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/instances.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/instances.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/operations.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/operations.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/history.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/history.js') }}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user