v1.0.90: Steps 1-4 — auth.py, dedup, httponly cookie, _op_results TTL

This commit is contained in:
2026-07-27 22:56:51 +04:00
parent 9085c9088e
commit f6a7bf76c5
4 changed files with 123 additions and 87 deletions
+34 -40
View File
@@ -29,6 +29,7 @@ import os
import fcntl
from api.http_client import HttpClient, detect_endpoint, stand_name
from api.auth import get_token, get_client, get_client_id, get_stand
from operations.get_services import get_services, get_service_detail
from operations.get_instances import get_instances
from operations.tracker import add as tracker_add, list_all as tracker_list
@@ -92,37 +93,10 @@ def _unique_display_name(client, requested_name):
return candidate
def _client():
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
return HttpClient(endpoint, token)
def _client_id():
"""Извлечь ClientID из JWT токена (из cookie или env)."""
import base64, json
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
try:
parts = token.split(".")
if len(parts) >= 2:
payload = base64.urlsafe_b64decode(parts[1] + "==")
return json.loads(payload).get("ClientID", "?")
except Exception:
pass
return "?"
def _stand():
"""dev/test — по токену."""
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
return stand_name(endpoint)
@bp.route("/api/services")
def api_services():
try:
raw = get_services(_client())
raw = get_services(get_client())
svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw]
svc_list.sort(key=lambda s: s["svcId"])
return jsonify(svc_list)
@@ -133,7 +107,7 @@ def api_services():
@bp.route("/api/instances/list")
def api_instances_list():
try:
raw = get_instances(_client())
raw = get_instances(get_client())
inst = [{"instanceUid": i["instanceUid"], "displayName": i["displayName"], "serviceId": i["serviceId"], "svc": i["svc"], "explainedStatus": i.get("explainedStatus", "?")} for i in raw]
return jsonify(inst)
except Exception as e:
@@ -143,13 +117,13 @@ def api_instances_list():
@bp.route("/api/operations/<int:svc_id>")
def api_operations(svc_id):
try:
detail = get_service_detail(_client(), svc_id)
detail = get_service_detail(get_client(), svc_id)
ops = detail.get("operations", [])
# только отслеживаемые инстансы этого сервиса
tracked = tracker_list(_client_id(), _stand())
tracked = tracker_list(get_client_id(), get_stand())
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
tracked_uids = set(tracked_by_uid.keys())
instances = get_instances(_client())
instances = get_instances(get_client())
nubes_uids = {i["instanceUid"] for i in instances}
svc_instances = [i for i in instances
if i.get("instanceUid") in tracked_uids
@@ -182,7 +156,7 @@ def api_params(op_id):
if instance_uid:
# Текущие значения: GET /instances/{uid} → state.params + шаблон → слияние
result = get_params_with_current_values(_client(), op_id, instance_uid)
result = get_params_with_current_values(get_client(), op_id, instance_uid)
_log(f"[api_params] MERGED returning {len(result)} params from state.params")
for p in result:
_log(f"[api_params] {p['name']}: defaultValue={p['defaultValue']!r}")
@@ -190,7 +164,7 @@ def api_params(op_id):
# Без instanceUid — шаблонные значения по умолчанию (CREATE)
_log(f"[api_params] DEFAULT branch: template defaults")
data = _client().get(f"/instanceOperations/default/{op_id}")
data = get_client().get(f"/instanceOperations/default/{op_id}")
params = data["svcOperation"]["cfsParams"]
result = []
for p in params:
@@ -226,7 +200,7 @@ def api_test():
instance_uid = data.get("instanceUid")
display_name = data.get("displayName") or ""
client = _client()
client = get_client()
try:
if op_name == "create":
@@ -248,7 +222,7 @@ def api_test():
# Записать в трекер СРАЗУ после получения instanceUid, до params/run
# (если params упадут — инстанс уже отслежен, сирот не будет)
try:
tracker_add(_client_id(), _stand(), instance_uid, svc_id, display_name)
tracker_add(get_client_id(), get_stand(), instance_uid, svc_id, display_name)
except Exception as e:
import traceback
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
@@ -260,7 +234,7 @@ def api_test():
client.post(f"/instanceOperations/{op_uid}/run")
# фоном ждать завершения
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True, _client_id(), _stand()), 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()), daemon=True).start()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
else:
@@ -290,7 +264,7 @@ def api_test():
# Получить реальное имя инстанса из API (не UUID!)
if not display_name:
display_name = _get_instance_display_name(client, instance_uid)
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, _client_id(), _stand(), is_delete), daemon=True).start()
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, get_client_id(), get_stand(), is_delete), daemon=True).start()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
except Exception as e:
@@ -298,8 +272,25 @@ def api_test():
return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]})
# Результаты фоновых операций: opUid → {status, error, stages, duration}
# Результаты фоновых операций: opUid → {status, error, stages, duration, _ts}
# _ts — timestamp добавления, для TTL-очистки (макс. 500 записей или старше 1 часа)
_op_results = {}
_MAX_OP_RESULTS = 500
def _cleanup_op_results():
"""Удалить старые записи: старше 1 часа или сверх лимита."""
import time
now = time.time()
# Удалить старше 1 часа
stale = [k for k, v in _op_results.items() if now - v.get("_ts", 0) > 3600]
for k in stale:
del _op_results[k]
# Если всё ещё много — удалить самые старые
if len(_op_results) > _MAX_OP_RESULTS:
sorted_keys = sorted(_op_results.keys(), key=lambda k: _op_results[k].get("_ts", 0))
for k in sorted_keys[:len(_op_results) - _MAX_OP_RESULTS]:
del _op_results[k]
def _get_instance_display_name(client, instance_uid):
@@ -315,6 +306,7 @@ 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):
"""Фоном ждать dtFinish и сохранить результат."""
import time
_cleanup_op_results() # очистить старые записи перед добавлением новой
t0 = time.time()
deadline = t0 + 300
while time.time() < deadline:
@@ -332,6 +324,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
"displayName": display_name,
"stages": op.get("stages", []),
"duration": round(time.time() - t0, 1),
"_ts": time.time(),
}
if dt_finish and str(dt_finish).strip():
is_ok = op.get("isSuccessful")
@@ -346,6 +339,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
"error": str(err) if err else "",
"stages": op.get("stages", []),
"duration": round(time.time() - t0, 1),
"_ts": time.time(),
}
return
time.sleep(5)
@@ -353,7 +347,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_
import traceback
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)}
_op_results[op_uid] = {"status": "TIMEOUT", "displayName": display_name, "duration": round(time.time() - t0, 1), "_ts": time.time()}
@bp.route("/api/test/status/<op_uid>")