feat: селектор режима и стенда (v1.2.30)

Новое:
- Селектор режима (🎭 Эмуляция / ☁️ Облако) над версией
- Селектор стенда (DEV/TEST/PROD) в режиме эмуляции
- В эмуляции — всё в полигон (/stand/api/v1/svc)
- В эмуляции — токен дезактивирован
- По умолчанию: эмуляция + TEST

Изменено:
- auth.py: get_mode(), get_polygon_stand(), get_client() с префиксом стенда
- main.py: action=set_mode, единый get_client() вместо real_client/inst_client
- index.html: селекторы, disable токена
- service_list.py: обрезка polygon_ префикса
- api_test.py: get_real_client → get_client

Задокументировано в HISTORY/2026-08-03-session.md.
This commit is contained in:
2026-08-03 08:50:14 +04:00
parent 78e1c4dc3b
commit 0a62eb3c58
6 changed files with 156 additions and 108 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ def _unique_display_name(client, requested_name):
@bp.route("/api/services")
def api_services():
try:
raw = get_services(get_real_client())
raw = get_services(get_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)
+69 -58
View File
@@ -11,7 +11,7 @@ GET /api/operations/<svc_id> — операции и autotest-инста
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 api.auth import get_token, get_client_id, get_token_info, get_token_masked, get_client, get_real_client, get_stand, get_mode, get_polygon_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
@@ -44,12 +44,17 @@ def index():
"""Главная страница: организация, инфраструктура, сервисы, форма токена.
GET — рендерит страницу с данными из API.
POST — обрабатывает форму токена (action=save/clear)."""
POST — обрабатывает форму токена (action=save/clear) или смену режима (action=set_mode)."""
# Режим и стенд — из cookie (см. auth.py)
mode = get_mode()
polygon_stand = get_polygon_stand()
polygon_enabled = bool(current_app.config.get("POLYGON_ENDPOINT", ""))
# Токены: 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 # пользовательский приоритетнее
active_token = user_token or env_token
org = None
error = None
@@ -72,10 +77,13 @@ def index():
"client_id": get_client_id(),
"token_info": get_token_info(),
"config": config,
"stand": stand,
"stand": stand,
"services": services,
"instances": instances,
"instance_groups": instance_groups,
"mode": mode,
"polygon_stand": polygon_stand,
"polygon_enabled": polygon_enabled,
}
ctx.update(overrides)
return render_template("index.html", **ctx)
@@ -91,63 +99,69 @@ def index():
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.set_cookie("token", user_token, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
resp.headers["Location"] = "/"
resp.status_code = 302
return resp
# Загрузка данных из API (только если есть токен)
# Обработка action=set_mode: сохранить режим и стенд в cookie
if action == "set_mode":
new_mode = request.form.get("mode", "polygon")
new_stand = request.form.get("polygon_stand", "test")
resp = make_response(redirect("/"))
resp.set_cookie("mode", new_mode, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
resp.set_cookie("polygon_stand", new_stand, max_age=60*60*24*365, httponly=True, samesite="Strict", secure=True)
return resp
# Загрузка данных из API
# В эмуляции — без проверки токена (полигон не требует авторизации)
# В облаке — только если есть токен
services = []
instances = []
instance_groups = {}
config = {}
config["VERSION"] = current_app.config["VERSION"] # всегда, даже без токена
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"]
if mode == "polygon" or active_token:
client = get_client()
inst_client = client # единый клиент — и сервисы, и инстансы
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", "")))
try:
# Организация — инстанс с serviceId=19
org = get_organization(client)
# Все сервисы — из полигона (эмуляция) или реального API (облако)
raw_svc = get_services(client)
services = sorted(raw_svc, key=lambda s: (s.get("svcId", 0), s.get("svc", "")))
# Инфраструктурные инстансы
# 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]
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 = "Токен невалиден или просрочен"
# Группировка по типу сервиса для левой колонки 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 не переотправлял форму
# После POST (save/clear/set_mode) — редирект на GET
if request.method == "POST":
return redirect("/")
return _tmpl()
@@ -170,22 +184,19 @@ def api_operations(svc_id):
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"]
# Единый клиент — и сервисы, и инстансы (полигон или реальный API)
client = get_client()
# Детали сервиса: список операций (modify, delete, suspend, ...)
detail = get_service_detail(real_client, svc_id)
detail = get_service_detail(client, svc_id)
ops = detail.get("operations", [])
# Трекер: наши autotest-инстансы (изолирован по пользователю и стенду)
tracked = tracker_list(get_client_id(), stand_name(endpoint))
tracked = tracker_list(get_client_id(), get_stand())
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
# Все инстансы — из polygon или реального API
instances = get_instances(inst_client)
# Все инстансы — из полигона или реального API
instances = get_instances(client)
nubes_uids = {i["instanceUid"] for i in instances}
svc_instances = []