Files

239 lines
12 KiB
Python
Raw Permalink 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
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:
# create_client — автоопределение стенда + HttpClient
result = create_client(active_token, current_app.config["NUBES_API_ENDPOINT"])
if result:
client, endpoint = result
stand = stand_name(endpoint)
try:
# Организация — инстанс с serviceId=19
org = get_organization(client)
# Все сервисы — сортировка по svcId
raw_svc = get_services(client)
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
# Инфраструктурные инстансы — только определённые serviceId
# 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(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:
# Автоопределение стенда + HttpClient
result = create_client(active_token, current_app.config["NUBES_API_ENDPOINT"])
if not result:
return jsonify({"error": "Не удалось определить стенд"}), 500
client, endpoint = result
# Детали сервиса: список операций (modify, delete, suspend, ...)
detail = get_service_detail(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}
# Все инстансы из облака
instances = get_instances(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