v1.1.45: phase1 — escape fixes, refSvc top-level, redact secrets, validate inputs, no traceback, TIMEOUT save, remove legacy api_bp

This commit is contained in:
2026-07-30 13:43:48 +04:00
parent 045ca438c6
commit 0ab6b3a89e
3 changed files with 78 additions and 19 deletions
+72 -11
View File
@@ -46,6 +46,22 @@ 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)
@@ -163,12 +179,24 @@ def api_test():
import threading, time
data = request.get_json()
svc_id = data["serviceId"]
op_name = data["operation"]
svc_op_id = data["svcOperationId"]
svc_id = data.get("serviceId")
op_name = (data.get("operation") or "").strip()
svc_op_id = data.get("svcOperationId")
params = data.get("params", {})
instance_uid = data.get("instanceUid")
display_name = data.get("displayName") or ""
instance_uid = (data.get("instanceUid") or "").strip()
display_name = (data.get("displayName") or "").strip()
# --- Validation ---
if not isinstance(svc_id, int) or svc_id <= 0:
return jsonify({"status": "FAIL", "error": "invalid serviceId"}), 400
if not op_name:
return jsonify({"status": "FAIL", "error": "invalid operation name"}), 400
if not isinstance(svc_op_id, int) or svc_op_id <= 0:
return jsonify({"status": "FAIL", "error": "invalid svcOperationId"}), 400
if instance_uid and len(instance_uid) != 36:
return jsonify({"status": "FAIL", "error": "invalid instanceUid (must be UUID)"}), 400
if not isinstance(params, dict):
return jsonify({"status": "FAIL", "error": "invalid params (must be object)"}), 400
client = get_client()
@@ -253,7 +281,8 @@ def api_test():
except Exception as e:
import traceback
return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]})
_log(f"[api_test] EXCEPTION: {traceback.format_exc()}")
return jsonify({"status": "FAIL", "error": str(e)}), 500
# Результаты фоновых операций: opUid → {status, error, stages, duration, _ts}
@@ -295,14 +324,30 @@ def _normalize_value(val, data_type, data_descriptor):
def _resolve_ref_svc(client, cfs_params, params):
"""resolveRefSvcParamValues — Terraform step 4.
Для параметров с refSvcId в dataDescriptor: если значение пустое —
подставить UUID существующего инстанса этого сервиса."""
Для параметров с 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
pid = str(p["svcOperationCfsParamId"])
user_val = {}
if pid in params:
try:
@@ -321,7 +366,9 @@ def _resolve_ref_svc(client, cfs_params, params):
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)
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:
@@ -438,6 +485,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 {})
save_run(client_id, stand,
user_email,
svc_id, op.get("svc", ""), op_name, svc_op_id,
@@ -445,7 +493,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
"OK" if is_ok else "FAIL",
round(time.time() - t0, 1),
str(err) if err else "",
params or {},
saved_params,
op.get("stages", []),
app_version)
except Exception as e:
@@ -459,6 +507,19 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
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:
from db.save_run import save_run
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,
"TIMEOUT", round(time.time() - t0, 1),
"TIMEOUT after 1800s", saved_params, [], app_version)
except Exception as e:
import traceback
print(f"[DB] TIMEOUT save_run FAILED: {e}", flush=True)
traceback.print_exc()
@bp.route("/api/test/status/<op_uid>")