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
This commit is contained in:
2026-07-30 22:02:22 +04:00
parent 189a2779d6
commit a96658b9b5
18 changed files with 1126 additions and 706 deletions
+5 -148
View File
@@ -34,6 +34,7 @@ from api.auth import get_token, get_client, get_client_id, get_stand, get_token_
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.tracker import add as tracker_add, list_all as tracker_list
from operations.tracker import remove as tracker_remove
@@ -46,22 +47,6 @@ MAX_LOG_SIZE = 512 * 1024 # 512 KB — ротация
MAX_LOG_LINES = 200 # сколько отдавать в /api/log
REDACT_KEYS = {"password", "secret", "token", "key", "privatekey", "private_key", "passwd", "pass"}
def _redact_params(params):
"""Заменить значения секретных полей на *** перед сохранением в БД."""
if not params:
return params
result = {}
for k, v in params.items():
if any(r in str(k).lower() for r in REDACT_KEYS):
result[k] = "***"
else:
result[k] = v
return result
def _log(msg):
"""Пишет в stdout (gunicorn) и в файл (UI-панель, общий для всех воркеров)."""
print(msg, flush=True)
@@ -228,7 +213,7 @@ def api_test():
traceback.print_exc()
# Отправка параметров (шаги 3-7 Terraform)
_send_params_terraform(client, op_uid, params)
send_params_terraform(client, op_uid, params)
client.post(f"/instanceOperations/{op_uid}/run")
@@ -267,7 +252,7 @@ def api_test():
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)
send_params_terraform(client, op_uid, params)
client.post(f"/instanceOperations/{op_uid}/run")
# фоном ждать завершения
@@ -291,134 +276,6 @@ _op_results = {}
_MAX_OP_RESULTS = 500
def _normalize_value(val, data_type, data_descriptor):
"""normalizeUniversalValueV6 — Terraform-equivalent.
Пустые значения нормализуются по типу: integer→"0", boolean→"false",
array→"[]", map/json→"{}", map-fixed→сборка из sub-param defaults."""
val = (val or "").strip()
if val.lower() == "null" or val == '""':
val = ""
dt = (data_type or "").lower()
# map-fixed/map с DataDescriptor: пусто или {} → собрать из sub-param defaults
if ("map" in dt) and isinstance(data_descriptor, dict) and data_descriptor:
if not val or val == "{}":
sub_obj = {}
for sk, sv in data_descriptor.items():
sd = sv.get("defaultValue")
sub_obj[sk] = str(sd) if sd is not None else ""
return json.dumps(sub_obj)
return val
if val:
return val
# Пустое — нормализовать по типу
if "array" in dt:
return "[]"
if "map" in dt or "json" in dt:
return "{}"
if "integer" in dt or "int" in dt:
return "0"
if "boolean" in dt or "bool" in dt:
return "false"
return val # пустая строка — отправляем как есть (Terraform тоже шлёт)
def _resolve_ref_svc(client, cfs_params, params):
"""resolveRefSvcParamValues — Terraform step 4.
Для параметров с refSvcId (верхнеуровневый ИЛИ в dataDescriptor):
если значение пустое — подставить UUID существующего инстанса этого сервиса."""
instances = None # lazy load
for p in cfs_params:
pid = str(p["svcOperationCfsParamId"])
# --- Верхнеуровневый refSvcId ---
top_ref = p.get("refSvcId")
if top_ref:
cur = (params.get(pid) or "").strip()
if not cur or cur == '""':
if instances is None:
try:
instances = get_instances(client)
except Exception:
return
match = next((i for i in instances
if i.get("serviceId") == top_ref
and i.get("explainedStatus") not in ("deleted",)), None)
if match:
params[pid] = match["instanceUid"]
# --- Вложенный refSvcId в dataDescriptor ---
dd = p.get("dataDescriptor")
if not isinstance(dd, dict):
continue
user_val = {}
if pid in params:
try:
user_val = json.loads(params[pid]) if params[pid] else {}
except (json.JSONDecodeError, ValueError):
user_val = {}
for sk, sv in dd.items():
ref_id = sv.get("refSvcId")
if not ref_id:
continue
cur = user_val.get(sk, "")
if cur and cur != '""':
continue # уже заполнено
if instances is None:
try:
instances = get_instances(client)
except Exception:
return
match = next((i for i in instances
if i.get("serviceId") == ref_id
and i.get("explainedStatus") not in ("deleted",)), None)
if match:
user_val[sk] = match["instanceUid"]
if user_val:
params[pid] = json.dumps(user_val)
def _send_params_terraform(client, op_uid, params):
"""Terraform steps 3-7: cfsParams → resolveRefSvc → send user → send all unsent → validate.
Работает для CREATE и non-CREATE одинаково."""
# Шаг 3: GET cfsParams реальной операции
op_det = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams")
cfs_params = op_det.get("instanceOperation", {}).get("cfsParams", [])
# Шаг 4: resolveRefSvcParamValues
_resolve_ref_svc(client, cfs_params, params)
# Шаг 5: отправить ВСЕ пользовательские params (включая пустые!)
sent = set()
for pid, pval in params.items():
client.post("/instanceOperationCfsParams", {
"instanceOperationUid": op_uid,
"svcOperationCfsParamId": int(pid),
"paramValue": str(pval)
})
sent.add(int(pid))
# Шаг 6: дослать ВСЕ неотправленные params из cfsParams
for p in cfs_params:
pid = p["svcOperationCfsParamId"]
if pid in sent:
continue
# nil-check: paramValue может быть None (отличаем от "")
val = p.get("paramValue")
if val is None:
val = p.get("defaultValue")
if val is None:
val = ""
val = _normalize_value(str(val), p.get("dataType", ""), p.get("dataDescriptor"))
client.post("/instanceOperationCfsParams", {
"instanceOperationUid": op_uid,
"svcOperationCfsParamId": pid,
"paramValue": val
})
# Шаг 7: validate-cfs (пустой ответ = успех)
try:
client.get(f"/instanceOperations/{op_uid}/validate-cfs")
except Exception as e:
if "Expecting value" in str(e) or "JSON" in str(type(e).__name__):
pass # пустой ответ = OK
else:
raise
def _cleanup_op_results():
"""Удалить старые записи: старше 1 часа или сверх лимита."""
import time
@@ -485,7 +342,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
# Сохранить в БД историю
try:
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,
svc_id, op.get("svc", ""), op_name, svc_op_id,
@@ -510,7 +367,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
# Сохранить TIMEOUT в БД
try:
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,
svc_id, "", op_name, svc_op_id,
op_uid, instance_uid, display_name,