Files
app-autotest/site/routes/api_test.py
T

332 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Blueprint, current_app, jsonify, request
import uuid
from api.http_client import HttpClient, detect_endpoint
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
bp = Blueprint("api_test", __name__)
AUTOTEST_PREFIX = "autotest-"
def _with_prefix(name):
name = (name or "").strip() or "instance"
return name if name.startswith(AUTOTEST_PREFIX) else f"{AUTOTEST_PREFIX}{name}"
def _unique_display_name(client, requested_name):
base_name = _with_prefix(requested_name)
try:
existing = {
i.get("displayName", "")
for i in get_instances(client)
if str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX)
}
except Exception:
existing = set()
if base_name not in existing:
return base_name
while True:
candidate = f"{base_name}-{uuid.uuid4().hex[:6]}"
if candidate not in existing:
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)
@bp.route("/api/services")
def api_services():
try:
raw = get_services(_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)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/instances/list")
def api_instances_list():
try:
raw = get_instances(_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:
return jsonify({"error": str(e)}), 500
@bp.route("/api/operations/<int:svc_id>")
def api_operations(svc_id):
try:
detail = get_service_detail(_client(), svc_id)
ops = detail.get("operations", [])
# только отслеживаемые инстансы этого сервиса
tracked = tracker_list()
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())
nubes_uids = {i["instanceUid"] for i in instances}
svc_instances = [i for i in instances
if i.get("instanceUid") in tracked_uids
and i.get("explainedStatus") not in ("deleted",)]
# Добавить tracked инстансы, которых Nubes ещё не отдаёт (creating)
for uid, t in tracked_by_uid.items():
if uid not in nubes_uids:
svc_instances.append({
"instanceUid": uid,
"displayName": t["displayName"],
"serviceId": svc_id,
"svc": detail.get("svc", ""),
"explainedStatus": "creating",
})
return jsonify({
"svc": detail.get("svc", ""),
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
"instances": svc_instances,
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/params/<int:op_id>")
def api_params(op_id):
try:
instance_uid = request.args.get("instanceUid")
op_name = request.args.get("opName")
if instance_uid and op_name:
# Создать pending-операцию чтобы прочитать ТЕКУЩИЕ значения параметров инстанса
op_payload = {"instanceUid": instance_uid, "svcOperationId": op_id, "operation": op_name}
op_resp = _client().post("/instanceOperations", op_payload)
preview_op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not preview_op_uid:
return jsonify({"error": "Не удалось создать preview-операцию"}), 500
op_data = _client().get(f"/instanceOperations/{preview_op_uid}")
cfs_params = op_data.get("instanceOperation", {}).get("cfsParams", [])
result = []
for p in cfs_params:
dd = p.get("dataDescriptor")
result.append({
"svcOperationCfsParamId": p["svcOperationCfsParamId"],
"name": p.get("svcOperationCfsParam", ""),
"dataType": p.get("dataType", ""),
"isRequired": p.get("isRequired", False),
"defaultValue": p.get("paramValue"), # текущее значение из инстанса
"valueList": p.get("valueList"),
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None,
})
return jsonify({"params": result, "previewOpUid": preview_op_uid})
# Без instanceUid — шаблонные значения по умолчанию
data = _client().get(f"/instanceOperations/default/{op_id}")
params = data["svcOperation"]["cfsParams"]
result = []
for p in params:
dd = p.get("dataDescriptor")
result.append({
"svcOperationCfsParamId": p["svcOperationCfsParamId"],
"name": p.get("svcOperationCfsParam", ""),
"dataType": p.get("dataType", ""),
"isRequired": p.get("isRequired", False),
"defaultValue": p.get("defaultValue"),
"valueList": p.get("valueList"),
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None,
})
return jsonify({"params": result})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/test", methods=["POST"])
def api_test():
"""Запустить операцию — возвращает opUid сразу, выполнение в фоне."""
import threading, time
data = request.get_json()
svc_id = data["serviceId"]
op_name = data["operation"]
svc_op_id = data["svcOperationId"]
params = data.get("params", {})
instance_uid = data.get("instanceUid")
display_name = data.get("displayName") or ""
preview_op_uid = data.get("previewOpUid")
client = _client()
try:
if op_name == "create":
if not display_name:
display_name = f"autotest-{svc_id}"
display_name = _unique_display_name(client, display_name)
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
resp = client.post("/instances", payload)
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
if not instance_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
op_payload = {"instanceUid": instance_uid, "operation": "create"}
op_resp = client.post("/instanceOperations", op_payload)
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not op_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
# Записать в трекер СРАЗУ после получения instanceUid, до params/run
# (если params упадут — инстанс уже отслежен, сирот не будет)
try:
tracker_add(instance_uid, svc_id, display_name)
except Exception as e:
import traceback
print(f"[TRACKER ERROR] add failed: {e}", flush=True)
traceback.print_exc()
for pid, pval in params.items():
client.post("/instanceOperationCfsParams",
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
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()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
else:
if not instance_uid:
return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400
if preview_op_uid:
# Переиспользовать preview-операцию (уже создана в api_params)
op_uid = preview_op_uid
for pid, pval in params.items():
client.post("/instanceOperationCfsParams",
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
client.post(f"/instanceOperations/{op_uid}/run")
elif op_name == "redeploy":
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
op_resp = client.post("/instanceOperations", op_payload)
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not op_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
client.post(f"/instanceOperations/{op_uid}/run")
else:
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
op_resp = client.post("/instanceOperations", op_payload)
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
if not op_uid:
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
for pid, pval in params.items():
client.post("/instanceOperationCfsParams",
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
client.post(f"/instanceOperations/{op_uid}/run")
# фоном ждать завершения
is_delete = (op_name == "delete")
display_name = display_name or instance_uid or f"instance-{svc_id}"
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()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
except Exception as e:
import traceback
return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]})
# Результаты фоновых операций: opUid → {status, error, stages, duration}
_op_results = {}
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
"""Фоном ждать dtFinish и сохранить результат."""
import time
t0 = time.time()
deadline = t0 + 300
while time.time() < deadline:
try:
try:
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
except Exception:
time.sleep(5)
continue
op = data.get("instanceOperation", {})
dt_finish = op.get("dtFinish")
# Обновляем stages по мере появления
_op_results[op_uid] = {
"status": "RUNNING",
"displayName": display_name,
"stages": op.get("stages", []),
"duration": round(time.time() - t0, 1),
}
if dt_finish and str(dt_finish).strip():
is_ok = op.get("isSuccessful")
err = op.get("errorLog") or ""
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)
_op_results[op_uid] = {
"status": "OK" if is_ok else "FAIL",
"displayName": display_name,
"error": str(err) if err else "",
"stages": op.get("stages", []),
"duration": round(time.time() - t0, 1),
}
return
time.sleep(5)
except Exception:
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)}
@bp.route("/api/test/status/<op_uid>")
def api_test_status(op_uid):
"""Получить текущий статус операции (поллинг с UI)."""
# сначала проверяем фоновый трекер
if op_uid in _op_results:
return jsonify(_op_results[op_uid])
# иначе спрашиваем API напрямую
try:
data = _client().get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
op = data.get("instanceOperation", {})
dt_finish = op.get("dtFinish")
done = bool(dt_finish and str(dt_finish).strip())
return jsonify({
"status": "OK" if (done and op.get("isSuccessful")) else ("FAIL" if done else "RUNNING"),
"done": done,
"displayName": op.get("displayName"),
"isSuccessful": op.get("isSuccessful"),
"isInProgress": op.get("isInProgress"),
"duration": op.get("duration"),
"stages": op.get("stages", []),
"errorLog": op.get("errorLog"),
})
except Exception as e:
return jsonify({"error": str(e)}), 500
def _find_uid(resp):
"""Извлечь instanceUid или instanceOperationUid из ответа API."""
if isinstance(resp, dict):
for key in ("instanceOperationUid", "instanceUid", "uid", "Uid"):
v = resp.get(key)
if isinstance(v, str) and v:
return v
return None
def _uid_from_location(loc):
if loc:
parts = loc.rstrip("/").split("/")
if len(parts[-1]) == 36:
return parts[-1]
return None