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:
+1
-3
@@ -14,18 +14,16 @@ import os
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from routes.main import bp as main_bp
|
from routes.main import bp as main_bp
|
||||||
from routes.api import bp as api_bp
|
|
||||||
from routes.api_test import bp as api_test_bp
|
from routes.api_test import bp as api_test_bp
|
||||||
|
|
||||||
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
# Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI.
|
||||||
VERSION = "1.1.44"
|
VERSION = "1.1.45"
|
||||||
|
|
||||||
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")
|
||||||
app.config["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "")
|
app.config["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "")
|
||||||
app.config["VERSION"] = VERSION
|
app.config["VERSION"] = VERSION
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
app.register_blueprint(api_bp)
|
|
||||||
app.register_blueprint(api_test_bp)
|
app.register_blueprint(api_test_bp)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+72
-11
@@ -46,6 +46,22 @@ MAX_LOG_SIZE = 512 * 1024 # 512 KB — ротация
|
|||||||
MAX_LOG_LINES = 200 # сколько отдавать в /api/log
|
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):
|
def _log(msg):
|
||||||
"""Пишет в stdout (gunicorn) и в файл (UI-панель, общий для всех воркеров)."""
|
"""Пишет в stdout (gunicorn) и в файл (UI-панель, общий для всех воркеров)."""
|
||||||
print(msg, flush=True)
|
print(msg, flush=True)
|
||||||
@@ -163,12 +179,24 @@ def api_test():
|
|||||||
import threading, time
|
import threading, time
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
svc_id = data["serviceId"]
|
svc_id = data.get("serviceId")
|
||||||
op_name = data["operation"]
|
op_name = (data.get("operation") or "").strip()
|
||||||
svc_op_id = data["svcOperationId"]
|
svc_op_id = data.get("svcOperationId")
|
||||||
params = data.get("params", {})
|
params = data.get("params", {})
|
||||||
instance_uid = data.get("instanceUid")
|
instance_uid = (data.get("instanceUid") or "").strip()
|
||||||
display_name = data.get("displayName") or ""
|
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()
|
client = get_client()
|
||||||
|
|
||||||
@@ -253,7 +281,8 @@ def api_test():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
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}
|
# Результаты фоновых операций: 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):
|
def _resolve_ref_svc(client, cfs_params, params):
|
||||||
"""resolveRefSvcParamValues — Terraform step 4.
|
"""resolveRefSvcParamValues — Terraform step 4.
|
||||||
Для параметров с refSvcId в dataDescriptor: если значение пустое —
|
Для параметров с refSvcId (верхнеуровневый ИЛИ в dataDescriptor):
|
||||||
подставить UUID существующего инстанса этого сервиса."""
|
если значение пустое — подставить UUID существующего инстанса этого сервиса."""
|
||||||
instances = None # lazy load
|
instances = None # lazy load
|
||||||
for p in cfs_params:
|
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")
|
dd = p.get("dataDescriptor")
|
||||||
if not isinstance(dd, dict):
|
if not isinstance(dd, dict):
|
||||||
continue
|
continue
|
||||||
pid = str(p["svcOperationCfsParamId"])
|
|
||||||
user_val = {}
|
user_val = {}
|
||||||
if pid in params:
|
if pid in params:
|
||||||
try:
|
try:
|
||||||
@@ -321,7 +366,9 @@ def _resolve_ref_svc(client, cfs_params, params):
|
|||||||
instances = get_instances(client)
|
instances = get_instances(client)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
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:
|
if match:
|
||||||
user_val[sk] = match["instanceUid"]
|
user_val[sk] = match["instanceUid"]
|
||||||
if user_val:
|
if user_val:
|
||||||
@@ -438,6 +485,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
|
|||||||
# Сохранить в БД историю
|
# Сохранить в БД историю
|
||||||
try:
|
try:
|
||||||
from db.save_run import save_run
|
from db.save_run import save_run
|
||||||
|
saved_params = _redact_params(params or {})
|
||||||
save_run(client_id, stand,
|
save_run(client_id, stand,
|
||||||
user_email,
|
user_email,
|
||||||
svc_id, op.get("svc", ""), op_name, svc_op_id,
|
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",
|
"OK" if is_ok else "FAIL",
|
||||||
round(time.time() - t0, 1),
|
round(time.time() - t0, 1),
|
||||||
str(err) if err else "",
|
str(err) if err else "",
|
||||||
params or {},
|
saved_params,
|
||||||
op.get("stages", []),
|
op.get("stages", []),
|
||||||
app_version)
|
app_version)
|
||||||
except Exception as e:
|
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)
|
print(f"[ERROR] _finish_op crashed: {traceback.format_exc()}", flush=True)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
_op_results[op_uid] = {"status": "TIMEOUT", "displayName": display_name, "duration": round(time.time() - t0, 1), "_ts": time.time()}
|
_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>")
|
@bp.route("/api/test/status/<op_uid>")
|
||||||
|
|||||||
+5
-5
@@ -164,7 +164,7 @@ function showParams(opId,opName){
|
|||||||
}
|
}
|
||||||
const form=document.getElementById('params-form');
|
const form=document.getElementById('params-form');
|
||||||
const displayNameValue=isCreate?makeCreateDisplayName():'';
|
const displayNameValue=isCreate?makeCreateDisplayName():'';
|
||||||
const createHeader=isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">Создание: ${currentSvcName}</div>`:'';
|
const createHeader=isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">Создание: ${_esc(currentSvcName)}</div>`:'';
|
||||||
const opHeader=!isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">${_esc(opName)} → ${_esc(findInstName())}</div>`:'';
|
const opHeader=!isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">${_esc(opName)} → ${_esc(findInstName())}</div>`:'';
|
||||||
form.innerHTML=createHeader+opHeader+(opName==='create'?`<div class="param-row"><label class="req">displayName</label><span style="font-size:12px;color:var(--muted);white-space:nowrap;">${AUTOTEST_PREFIX}</span><input type="text" id="param-displayname" value="${_esc(displayNameValue)}" autocomplete="off"></div>`:'')
|
form.innerHTML=createHeader+opHeader+(opName==='create'?`<div class="param-row"><label class="req">displayName</label><span style="font-size:12px;color:var(--muted);white-space:nowrap;">${AUTOTEST_PREFIX}</span><input type="text" id="param-displayname" value="${_esc(displayNameValue)}" autocomplete="off"></div>`:'')
|
||||||
+ params.map(p=>{
|
+ params.map(p=>{
|
||||||
@@ -291,8 +291,8 @@ function setFinishedState(statusText, statusClass, statusError, statusDuration,
|
|||||||
btn.onclick=null;
|
btn.onclick=null;
|
||||||
const opName=selectedOp?.opName||'операция';
|
const opName=selectedOp?.opName||'операция';
|
||||||
const durationText=statusDuration!==undefined&&statusDuration!==null?`${statusDuration}s`:'0s';
|
const durationText=statusDuration!==undefined&&statusDuration!==null?`${statusDuration}s`:'0s';
|
||||||
const extra=statusError||'';
|
const extra=_esc(statusError||'');
|
||||||
const instName=instanceName||findInstName()||selectedInst||'';
|
const instName=_esc(instanceName||findInstName()||selectedInst||'');
|
||||||
document.getElementById('test-status').innerHTML=` <span class="badge ${statusClass}">${statusText}</span> <span style="color:var(--muted);">${opName}</span> <span style="color:var(--muted);">${durationText}</span> <span style="color:var(--muted);">${instName}</span> ${extra}`;
|
document.getElementById('test-status').innerHTML=` <span class="badge ${statusClass}">${statusText}</span> <span style="color:var(--muted);">${opName}</span> <span style="color:var(--muted);">${durationText}</span> <span style="color:var(--muted);">${instName}</span> ${extra}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ async function executeOp(params){
|
|||||||
displayName:isCreate ? displayName : ''
|
displayName:isCreate ? displayName : ''
|
||||||
})});
|
})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(d.status==='FAIL'){busy=false;document.getElementById('test-status').innerHTML=` <span class="badge">FAIL</span> ${d.error||''}`;btn.disabled=false;return;}
|
if(d.status==='FAIL'){busy=false;document.getElementById('test-status').innerHTML=` <span class="badge">FAIL</span> ${_esc(d.error||'')}`;btn.disabled=false;return;}
|
||||||
// CMDB delete — мгновенное удаление, не надо поллинга
|
// CMDB delete — мгновенное удаление, не надо поллинга
|
||||||
if(d.status==='OK'&&(d.opUid||'').startsWith('cmdb-')){
|
if(d.status==='OK'&&(d.opUid||'').startsWith('cmdb-')){
|
||||||
busy=false;
|
busy=false;
|
||||||
@@ -457,7 +457,7 @@ function startLogPoll(){
|
|||||||
try{
|
try{
|
||||||
const r=await fetch('/api/log');
|
const r=await fetch('/api/log');
|
||||||
const lines=await r.json();
|
const lines=await r.json();
|
||||||
el.innerHTML=lines.map(l=>`<div>${l}</div>`).join('');
|
el.innerHTML=lines.map(l=>`<div>${_esc(l)}</div>`).join('');
|
||||||
el.scrollTop=el.scrollHeight;
|
el.scrollTop=el.scrollHeight;
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
},2000);
|
},2000);
|
||||||
|
|||||||
Reference in New Issue
Block a user