241 lines
12 KiB
Python
241 lines
12 KiB
Python
"""
|
||
Роуты главной страницы и списка операций.
|
||
|
||
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
|