diff --git a/site/app.py b/site/app.py index a8c6f24..141f4c1 100644 --- a/site/app.py +++ b/site/app.py @@ -6,7 +6,7 @@ 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 -VERSION = "1.0.85" +VERSION = "1.0.86" app = Flask(__name__, template_folder="templates", static_folder="static") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc") diff --git a/site/operations/tracker.py b/site/operations/tracker.py index a6c2bd3..9809256 100644 --- a/site/operations/tracker.py +++ b/site/operations/tracker.py @@ -1,17 +1,12 @@ -"""Трекер созданных инстансов — файловый с fcntl.flock (multi-worker gunicorn).""" +"""Трекер созданных инстансов — по юзеру и стенду: /tmp/instances-{client_id}-{stand}.json.""" import fcntl import json import os import time -_PATH = "/tmp/instances.json" -_INITIAL = { - "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d": {"svcId": 1, "displayName": "autotest-1", "instanceUid": "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d"}, - "884356cf-5212-4395-b56b-27c58a5fc1fa": {"svcId": 1, "displayName": "autotest-1-first", "instanceUid": "884356cf-5212-4395-b56b-27c58a5fc1fa"}, - "6528854d-a4ea-428c-9fa4-68e85fa9b3ac": {"svcId": 1, "displayName": "curl-test-144834", "instanceUid": "6528854d-a4ea-428c-9fa4-68e85fa9b3ac"}, - "fdcb5887-39f7-4ef8-a9c0-d55e434a55ff": {"svcId": 1, "displayName": "curl-final-test", "instanceUid": "fdcb5887-39f7-4ef8-a9c0-d55e434a55ff"}, -} +def _path(client_id, stand): + return f"/tmp/instances-{client_id}-{stand}.json" def _acquire_lock(fd): @@ -27,36 +22,31 @@ def _acquire_lock(fd): time.sleep(0.05) -def _locked_read(): - """Читает файл под эксклюзивной блокировкой, seed при отсутствии.""" +def _locked_read(path): + """Читает файл под эксклюзивной блокировкой, пустой dict если нет.""" try: - fd = os.open(_PATH, os.O_RDWR | os.O_CREAT, 0o644) + fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644) except OSError: - return dict(_INITIAL) + return {} try: if not _acquire_lock(fd): - return dict(_INITIAL) + return {} try: data = os.read(fd, 65536) if data: return json.loads(data.decode("utf-8")) except (json.JSONDecodeError, UnicodeDecodeError): pass - # Файл пуст или битый — пишем _INITIAL - data = dict(_INITIAL) - os.lseek(fd, 0, 0) - os.ftruncate(fd, 0) - os.write(fd, json.dumps(data, indent=2).encode("utf-8")) - return data + return {} finally: fcntl.flock(fd, fcntl.LOCK_UN) os.close(fd) -def _locked_write(data): +def _locked_write(path, data): """Пишет файл под эксклюзивной блокировкой.""" try: - fd = os.open(_PATH, os.O_RDWR | os.O_CREAT, 0o644) + fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o644) except OSError: return try: @@ -70,21 +60,23 @@ def _locked_write(data): os.close(fd) -def add(instance_uid, svc_id, display_name): - data = _locked_read() +def add(client_id, stand, instance_uid, svc_id, display_name): + p = _path(client_id, stand) + data = _locked_read(p) data[instance_uid] = { "svcId": svc_id, "displayName": display_name, "instanceUid": instance_uid, } - _locked_write(data) + _locked_write(p, data) -def remove(instance_uid): - data = _locked_read() +def remove(client_id, stand, instance_uid): + p = _path(client_id, stand) + data = _locked_read(p) data.pop(instance_uid, None) - _locked_write(data) + _locked_write(p, data) -def list_all(): - return list(_locked_read().values()) +def list_all(client_id, stand): + return list(_locked_read(_path(client_id, stand)).values()) diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 8af7913..42c7b7a 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -3,12 +3,11 @@ import uuid import os import fcntl -from api.http_client import HttpClient, detect_endpoint +from api.http_client import HttpClient, detect_endpoint, stand_name 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 from operations.tracker import remove as tracker_remove -from operations.get_params import get_params_with_current_values bp = Blueprint("api_test", __name__) @@ -74,6 +73,27 @@ def _client(): 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: @@ -101,7 +121,7 @@ def api_operations(svc_id): detail = get_service_detail(_client(), svc_id) ops = detail.get("operations", []) # только отслеживаемые инстансы этого сервиса - tracked = tracker_list() + tracked = tracker_list(_client_id(), _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()) @@ -203,7 +223,7 @@ def api_test(): # Записать в трекер СРАЗУ после получения instanceUid, до params/run # (если params упадут — инстанс уже отслежен, сирот не будет) try: - tracker_add(instance_uid, svc_id, display_name) + tracker_add(_client_id(), _stand(), instance_uid, svc_id, display_name) except Exception as e: import traceback print(f"[TRACKER ERROR] add failed: {e}", flush=True) @@ -215,7 +235,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), daemon=True).start() + 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() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name}) else: @@ -245,7 +265,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, 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, _client_id(), _stand(), is_delete), daemon=True).start() return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name}) except Exception as e: @@ -267,7 +287,7 @@ def _get_instance_display_name(client, instance_uid): return instance_uid -def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False): +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 t0 = time.time() @@ -294,7 +314,7 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_ print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True) # tracker_add для create уже вызван синхронно в api_test() if is_ok and is_delete: - tracker_remove(instance_uid) + tracker_remove(client_id, stand, instance_uid) _op_results[op_uid] = { "status": "OK" if is_ok else "FAIL", "displayName": display_name, diff --git a/site/routes/main.py b/site/routes/main.py index e3680c3..1512607 100644 --- a/site/routes/main.py +++ b/site/routes/main.py @@ -155,10 +155,10 @@ def api_operations(svc_id): result = create_client(active_token, current_app.config["NUBES_API_ENDPOINT"]) if not result: return jsonify({"error": "Не удалось определить стенд"}), 500 - client, _ = result + client, endpoint = result detail = get_service_detail(client, svc_id) ops = detail.get("operations", []) - tracked = tracker_list() + tracked = tracker_list(_client_id(active_token), stand_name(endpoint)) tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id} instances = get_instances(client) nubes_uids = {i["instanceUid"] for i in instances}