v1.0.86: per-user per-stand tracker — /tmp/instances-{client_id}-{stand}.json, no _INITIAL
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
|
|||||||
from routes.api import bp as api_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
|
||||||
|
|
||||||
VERSION = "1.0.85"
|
VERSION = "1.0.86"
|
||||||
|
|
||||||
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-dev.ngcloud.ru/api/v1/svc")
|
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
|
||||||
|
|||||||
+21
-29
@@ -1,17 +1,12 @@
|
|||||||
"""Трекер созданных инстансов — файловый с fcntl.flock (multi-worker gunicorn)."""
|
"""Трекер созданных инстансов — по юзеру и стенду: /tmp/instances-{client_id}-{stand}.json."""
|
||||||
import fcntl
|
import fcntl
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
_PATH = "/tmp/instances.json"
|
|
||||||
|
|
||||||
_INITIAL = {
|
def _path(client_id, stand):
|
||||||
"408b7f96-6ed7-4985-be1b-f5bdcb6ab44d": {"svcId": 1, "displayName": "autotest-1", "instanceUid": "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d"},
|
return f"/tmp/instances-{client_id}-{stand}.json"
|
||||||
"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 _acquire_lock(fd):
|
def _acquire_lock(fd):
|
||||||
@@ -27,36 +22,31 @@ def _acquire_lock(fd):
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
def _locked_read():
|
def _locked_read(path):
|
||||||
"""Читает файл под эксклюзивной блокировкой, seed при отсутствии."""
|
"""Читает файл под эксклюзивной блокировкой, пустой dict если нет."""
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return dict(_INITIAL)
|
return {}
|
||||||
try:
|
try:
|
||||||
if not _acquire_lock(fd):
|
if not _acquire_lock(fd):
|
||||||
return dict(_INITIAL)
|
return {}
|
||||||
try:
|
try:
|
||||||
data = os.read(fd, 65536)
|
data = os.read(fd, 65536)
|
||||||
if data:
|
if data:
|
||||||
return json.loads(data.decode("utf-8"))
|
return json.loads(data.decode("utf-8"))
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
pass
|
pass
|
||||||
# Файл пуст или битый — пишем _INITIAL
|
return {}
|
||||||
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
|
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
def _locked_write(data):
|
def _locked_write(path, data):
|
||||||
"""Пишет файл под эксклюзивной блокировкой."""
|
"""Пишет файл под эксклюзивной блокировкой."""
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -70,21 +60,23 @@ def _locked_write(data):
|
|||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
def add(instance_uid, svc_id, display_name):
|
def add(client_id, stand, instance_uid, svc_id, display_name):
|
||||||
data = _locked_read()
|
p = _path(client_id, stand)
|
||||||
|
data = _locked_read(p)
|
||||||
data[instance_uid] = {
|
data[instance_uid] = {
|
||||||
"svcId": svc_id,
|
"svcId": svc_id,
|
||||||
"displayName": display_name,
|
"displayName": display_name,
|
||||||
"instanceUid": instance_uid,
|
"instanceUid": instance_uid,
|
||||||
}
|
}
|
||||||
_locked_write(data)
|
_locked_write(p, data)
|
||||||
|
|
||||||
|
|
||||||
def remove(instance_uid):
|
def remove(client_id, stand, instance_uid):
|
||||||
data = _locked_read()
|
p = _path(client_id, stand)
|
||||||
|
data = _locked_read(p)
|
||||||
data.pop(instance_uid, None)
|
data.pop(instance_uid, None)
|
||||||
_locked_write(data)
|
_locked_write(p, data)
|
||||||
|
|
||||||
|
|
||||||
def list_all():
|
def list_all(client_id, stand):
|
||||||
return list(_locked_read().values())
|
return list(_locked_read(_path(client_id, stand)).values())
|
||||||
|
|||||||
+28
-8
@@ -3,12 +3,11 @@ import uuid
|
|||||||
import os
|
import os
|
||||||
import fcntl
|
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_services import get_services, get_service_detail
|
||||||
from operations.get_instances import get_instances
|
from operations.get_instances import get_instances
|
||||||
from operations.tracker import add as tracker_add, list_all as tracker_list
|
from operations.tracker import add as tracker_add, list_all as tracker_list
|
||||||
from operations.tracker import remove as tracker_remove
|
from operations.tracker import remove as tracker_remove
|
||||||
from operations.get_params import get_params_with_current_values
|
|
||||||
|
|
||||||
bp = Blueprint("api_test", __name__)
|
bp = Blueprint("api_test", __name__)
|
||||||
|
|
||||||
@@ -74,6 +73,27 @@ def _client():
|
|||||||
return HttpClient(endpoint, token)
|
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")
|
@bp.route("/api/services")
|
||||||
def api_services():
|
def api_services():
|
||||||
try:
|
try:
|
||||||
@@ -101,7 +121,7 @@ def api_operations(svc_id):
|
|||||||
detail = get_service_detail(_client(), svc_id)
|
detail = get_service_detail(_client(), svc_id)
|
||||||
ops = detail.get("operations", [])
|
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_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
||||||
tracked_uids = set(tracked_by_uid.keys())
|
tracked_uids = set(tracked_by_uid.keys())
|
||||||
instances = get_instances(_client())
|
instances = get_instances(_client())
|
||||||
@@ -203,7 +223,7 @@ def api_test():
|
|||||||
# Записать в трекер СРАЗУ после получения instanceUid, до params/run
|
# Записать в трекер СРАЗУ после получения instanceUid, до params/run
|
||||||
# (если params упадут — инстанс уже отслежен, сирот не будет)
|
# (если params упадут — инстанс уже отслежен, сирот не будет)
|
||||||
try:
|
try:
|
||||||
tracker_add(instance_uid, svc_id, display_name)
|
tracker_add(_client_id(), _stand(), instance_uid, svc_id, display_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
|
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
|
||||||
@@ -215,7 +235,7 @@ def api_test():
|
|||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
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})
|
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -245,7 +265,7 @@ def api_test():
|
|||||||
# Получить реальное имя инстанса из API (не UUID!)
|
# Получить реальное имя инстанса из API (не UUID!)
|
||||||
if not display_name:
|
if not display_name:
|
||||||
display_name = _get_instance_display_name(client, instance_uid)
|
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})
|
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -267,7 +287,7 @@ def _get_instance_display_name(client, instance_uid):
|
|||||||
return 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 и сохранить результат."""
|
"""Фоном ждать dtFinish и сохранить результат."""
|
||||||
import time
|
import time
|
||||||
t0 = time.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)
|
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
|
||||||
# tracker_add для create уже вызван синхронно в api_test()
|
# tracker_add для create уже вызван синхронно в api_test()
|
||||||
if is_ok and is_delete:
|
if is_ok and is_delete:
|
||||||
tracker_remove(instance_uid)
|
tracker_remove(client_id, stand, instance_uid)
|
||||||
_op_results[op_uid] = {
|
_op_results[op_uid] = {
|
||||||
"status": "OK" if is_ok else "FAIL",
|
"status": "OK" if is_ok else "FAIL",
|
||||||
"displayName": display_name,
|
"displayName": display_name,
|
||||||
|
|||||||
+2
-2
@@ -155,10 +155,10 @@ def api_operations(svc_id):
|
|||||||
result = create_client(active_token, current_app.config["NUBES_API_ENDPOINT"])
|
result = create_client(active_token, current_app.config["NUBES_API_ENDPOINT"])
|
||||||
if not result:
|
if not result:
|
||||||
return jsonify({"error": "Не удалось определить стенд"}), 500
|
return jsonify({"error": "Не удалось определить стенд"}), 500
|
||||||
client, _ = result
|
client, endpoint = result
|
||||||
detail = get_service_detail(client, svc_id)
|
detail = get_service_detail(client, svc_id)
|
||||||
ops = detail.get("operations", [])
|
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}
|
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
||||||
instances = get_instances(client)
|
instances = get_instances(client)
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
nubes_uids = {i["instanceUid"] for i in instances}
|
||||||
|
|||||||
Reference in New Issue
Block a user