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

241 lines
12 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.
"""
Роуты главной страницы и списка операций.
GET/POST / — главная страница: организация, инфраструктура, токен
GET /api/operations/<svc_id> — операции и autotest-инстансы сервиса (cloud-first)
Вспомогательные функции:
_resolve_instance_status() — канонический статус (cloud или "creating" из трекера)
"""
from flask import Blueprint, current_app, render_template, request, make_response, jsonify, redirect
from api.http_client import HttpClient, detect_endpoint, create_client, stand_name
from api.auth import get_token, get_client_id, get_token_info, get_token_masked, get_client, get_real_client, get_stand
from operations.get_instances import get_organization, get_instances
from operations.get_services import get_services, get_service_detail
from operations.service_list import load_service_ids
from operations.tracker import list_all as tracker_list
from runner import load_config
bp = Blueprint("main", __name__)
# Префикс для autotest-инстансов — по нему фильтруем «наши» инстансы в облаке.
AUTOTEST_PREFIX = "autotest-"
def _resolve_instance_status(instance, tracked=None):
"""Канонический статус инстанса: explainedStatus из облака, или "creating" из трекера."""
tracked = tracked or {}
cloud_status = str(instance.get("explainedStatus") or "").strip()
display_name = str(instance.get("displayName") or "")
if cloud_status:
return cloud_status # облако знает статус — доверяем
if tracked and display_name.startswith(AUTOTEST_PREFIX):
return "creating" # в трекере есть, в облаке ещё нет — создаётся
return "unknown"
@bp.route("/", methods=["GET", "POST"])
def index():
"""Главная страница: организация, инфраструктура, сервисы, форма токена.
GET — рендерит страницу с данными из API.
POST — обрабатывает форму токена (action=save/clear)."""
# Токены: env — из переменной окружения, user — из cookie или формы
env_token = current_app.config["NUBES_API_TOKEN"]
user_token = request.cookies.get("token") or request.form.get("token") or ""
active_token = user_token or env_token # пользовательский приоритетнее
org = None
error = None
services = []
instances = []
instance_groups = {}
config = {}
action = request.form.get("action", "")
def _tmpl(**overrides):
"""Хелпер: собрать контекст для render_template.
Гарантирует что ВСЕ переменные переданы, даже если API не отработал.
overrides — можно переопределить отдельные переменные."""
ctx = {
"organization": org,
"error": error,
"env_token_masked": get_token_masked(),
"has_user_token": bool(user_token),
"client_id": get_client_id(),
"token_info": get_token_info(),
"config": config,
"stand": stand,
"services": services,
"instances": instances,
"instance_groups": instance_groups,
}
ctx.update(overrides)
return render_template("index.html", **ctx)
# Обработка action=clear: удалить cookie с токеном
if action == "clear":
resp = make_response(redirect("/"))
resp.delete_cookie("token")
return resp
# Обработка action=save: сохранить токен в cookie (на год)
if action == "save":
user_token = request.form.get("token", "")
active_token = user_token or env_token
resp = make_response()
resp.set_cookie("token", user_token, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True) # 1 год
resp.headers["Location"] = "/" # редирект на GET (убирает POST из истории)
resp.status_code = 302
return resp
# Загрузка данных из API (только если есть токен)
services = []
instances = []
instance_groups = {}
config = {}
config["VERSION"] = current_app.config["VERSION"] # всегда, даже без токена
config["service_ids"] = []
stand = "?"
if active_token:
# Сервисы — ВСЕГДА из реального API (метаданные)
real_client = get_real_client()
# Инстансы — из polygon если POLYGON_ENDPOINT задан
inst_client = get_client()
endpoint = current_app.config["NUBES_API_ENDPOINT"]
stand = get_stand()
if real_client:
try:
# Организация — инстанс с serviceId=19 (из реального API)
org = get_organization(real_client)
# Все сервисы — из реального API
raw_svc = get_services(real_client)
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
# Инфраструктурные инстансы — из polygon или реального API
# 19=Org, 21=vDC, 22=NSX-T, 25=External IP, 26=vApp, 29=vDC Group
# 2=Template, 12=S3, 110=?, 150=K8s
infra_ids = {2, 12, 21, 22, 25, 26, 29, 110, 150}
raw_inst = get_instances(inst_client)
instances = [i for i in raw_inst
if i.get("explainedStatus") not in ("deleted", "not created")
and i.get("serviceId") in infra_ids]
# Сортировка: организация (svcId=19) первая, остальные по имени
instances.sort(key=lambda i: (0 if i.get("serviceId") == 19 else 1, i.get("displayName", "")))
# Группировка по типу сервиса для левой колонки UI
instance_groups = {}
for i in instances:
svc_name = i.get("svc", "Прочее")
instance_groups.setdefault(svc_name, []).append(i)
# Конфиг из config.yaml + версия приложения
config = dict(load_config() or {})
config["VERSION"] = current_app.config["VERSION"]
# Список разрешённых сервисов из services_{stand}.txt
config["service_ids"] = sorted(load_service_ids(stand))
except Exception as e:
error = str(e)
else:
error = "Токен невалиден или просрочен"
# После POST (save/clear) — редирект на GET, чтобы F5 не переотправлял форму
if request.method == "POST":
return redirect("/")
return _tmpl()
@bp.route("/api/operations/<int:svc_id>")
def api_operations(svc_id):
"""Список операций и autotest-инстансов сервиса (cloud-first с fallback на трекер).
Вызывается из UI: selectService(), toggleInstance(), refreshInstances().
Алгоритм:
1. Cloud-first: фильтруем GET /instances по префиксу autotest-
2. Tracker-fallback: если инстанс есть в трекере но нет в облаке → status="creating"
3. Дедупликация: если displayName уже есть в cloud — tracker-запись пропускается"""
# Токен из cookie или env
env_token = current_app.config["NUBES_API_TOKEN"]
user_token = request.cookies.get("token") or ""
active_token = user_token or env_token
try:
# Сервисы — ВСЕГДА из реального API (метаданные)
real_client = get_real_client()
# Инстансы — из polygon если POLYGON_ENDPOINT задан
inst_client = get_client()
endpoint = current_app.config["NUBES_API_ENDPOINT"]
# Детали сервиса: список операций (modify, delete, suspend, ...)
detail = get_service_detail(real_client, svc_id)
ops = detail.get("operations", [])
# Трекер: наши autotest-инстансы (изолирован по пользователю и стенду)
tracked = tracker_list(get_client_id(), stand_name(endpoint))
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
# Все инстансы — из polygon или реального API
instances = get_instances(inst_client)
nubes_uids = {i["instanceUid"] for i in instances}
svc_instances = []
cloud_names = set() # для дедупликации с tracker
# --- Шаг 1: Cloud-first — фильтруем по префиксу autotest- ---
for i in instances:
display_name = str(i.get("displayName", "") or "")
if not display_name.startswith(AUTOTEST_PREFIX):
continue # не наш инстанс
if i.get("explainedStatus") in ("deleted",):
continue # удалённые не показываем
item = dict(i)
item["status"] = _resolve_instance_status(item, tracked_by_uid.get(i.get("instanceUid")))
svc_instances.append(item)
cloud_names.add(display_name)
# Сортировка: по имени сервиса, затем по дате создания (новые сверху)
svc_instances.sort(key=lambda i: (
i.get("svc", ""),
i.get("instanceConfigDtCreated") or ""
), reverse=True)
# reverse=True даёт обратный порядок по обоим ключам.
# Чтобы сервисы шли A→Я а даты новые→старые — пересортируем:
svc_instances.sort(key=lambda i: i.get("instanceConfigDtCreated") or "", reverse=True)
svc_instances.sort(key=lambda i: i.get("svc", ""))
# --- Шаг 2: Tracker-fallback ---
# Инстансы из трекера, которых ещё нет в облаке (только что созданы).
# Но если displayName уже есть в cloud_names — дубль, пропускаем.
for uid, t in tracked_by_uid.items():
if uid not in nubes_uids:
display_name = str(t.get("displayName", "") or "")
if display_name in cloud_names:
continue # дубль — облако уже показало этот инстанс
svc_instances.append({
"instanceUid": uid,
"displayName": display_name,
"serviceId": svc_id,
"svc": detail.get("svc", ""),
"explainedStatus": "",
"status": "creating", # наш статус — облако ещё не знает
})
return jsonify({
"svc": detail.get("svc", ""),
"svcShort": detail.get("svcShort", ""),
"operations": ops, # [{svcOperationId, operation}, ...]
"instances": svc_instances, # [{instanceUid, displayName, status, ...}, ...]
})
except Exception as e:
return jsonify({"error": str(e)}), 500