From 0a62eb3c5886fe016cb68a865d789bd2937b9c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 3 Aug 2026 08:50:14 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=B5=D0=BB=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BD=D0=B4=D0=B0=20(v1.2.30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новое: - Селектор режима (🎭 Эмуляция / ☁️ Облако) над версией - Селектор стенда (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. --- site/api/auth.py | 106 +++++++++++++++----------- site/app.py | 2 +- site/operations/service_list.py | 5 +- site/routes/api_test.py | 2 +- site/routes/main.py | 127 +++++++++++++++++--------------- site/templates/index.html | 22 +++++- 6 files changed, 156 insertions(+), 108 deletions(-) diff --git a/site/api/auth.py b/site/api/auth.py index 2bb4bb7..231b20e 100644 --- a/site/api/auth.py +++ b/site/api/auth.py @@ -5,13 +5,24 @@ в main.py и api_test.py с идентичным или почти идентичным кодом. Теперь всё здесь — один источник правды для всех роутов. +Режимы работы (2026-08-03): + - Эмуляция (mode=polygon): всё в полигон, стенд из селектора (dev/test/prod) + - Облако (mode=cloud): реальный API, стенд автоопределяется по токену + +Cookie (устанавливаются main.py:action=set_mode): + - mode — "polygon" (по умолчанию) или "cloud" + - polygon_stand — "test" (по умолчанию), "dev", "prod" + Функции (все без аргументов — берут данные из Flask request/current_app): - get_token() — токен: cookie → env-переменная - get_client() — HttpClient с автоопределением стенда - get_client_id() — ClientID из JWT (base64url, без проверки подписи) - get_stand() — "dev" / "test" по токену - get_token_info() — {email, company, client_id} из JWT для UI - get_token_masked() — маскированный токен (abc...xyz) для placeholder + get_mode() — "polygon" / "cloud" + get_polygon_stand() — "dev" / "test" / "prod" + get_token() — токен: cookie → env (в эмуляции только env) + get_client() — HttpClient с учётом режима и стенда + get_real_client() — алиас get_client() (историческая совместимость) + get_client_id() — ClientID из JWT (base64url, без проверки подписи) + get_stand() — "polygon_test" / "dev" / "test" + get_token_info() — {email, company, client_id} из JWT для UI + get_token_masked() — маскированный токен (abc...xyz) для placeholder """ import base64 @@ -21,57 +32,63 @@ from flask import request, current_app from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS +def get_mode(): + """Режим: 'polygon' (эмуляция) или 'cloud' (реальное облако). + + Если POLYGON_ENDPOINT не задан в конфиге — всегда облако (селектор не нужен). + Иначе — из cookie, по умолчанию эмуляция.""" + if not current_app.config.get("POLYGON_ENDPOINT", ""): + return "cloud" + return request.cookies.get("mode", "polygon") + + +def get_polygon_stand(): + """Стенд полигона: 'test' (по умолчанию), 'dev', 'prod'. + + Используется ТОЛЬКО в режиме эмуляции — для построения URL и load_service_ids.""" + return request.cookies.get("polygon_stand", "test") + + def get_token(): - """Получить активный токен: сначала из cookie пользователя, потом из env. + """Получить активный токен. - Приоритет: - 1. cookie "token" — пользователь ввёл свой токен в форме - 2. NUBES_API_TOKEN из env — сервисный токен (для автоматических тестов) - - Пользовательский токен приоритетнее — он переопределяет сервисный.""" + В эмуляции — ТОЛЬКО env-токен (пользовательский ввод дезактивирован). + В облаке — cookie → env (как раньше).""" + if get_mode() == "polygon": + return current_app.config["NUBES_API_TOKEN"] return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"] -def _make_client(*, use_polygon): - """Фабрика HttpClient — ЕДИНСТВЕННОЕ место где выбирается endpoint. +def get_client(): + """HttpClient с учётом режима и стенда. - Args: - use_polygon: если True и POLYGON_ENDPOINT задан → polygon. - Если False → всегда реальный API (для чтения сервисов/инстансов). - - Returns: - HttpClient с правильным endpoint и токеном. - """ + Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc + Облако: реальный API с автоопределением стенда (dev/test по токену).""" token = get_token() + mode = get_mode() - # Режим полигона — только если разрешено и переменная задана - if use_polygon: + if mode == "polygon": polygon = current_app.config.get("POLYGON_ENDPOINT", "") - if polygon: - return HttpClient(polygon, token) + stand = get_polygon_stand() + # POLYGON_ENDPOINT = "https://polygon.../api/v1/svc" + # → "https://polygon.../{stand}/api/v1/svc" + # rstrip на случай trailing slash в конфиге + return HttpClient(f"{polygon.rstrip('/')}/{stand}", token) - # Реальный API: автоопределение или NUBES_API_ENDPOINT + # Облако: NUBES_API_ENDPOINT с автоопределением endpoint = current_app.config["NUBES_API_ENDPOINT"] if endpoint in STANDS: endpoint = detect_endpoint(token) or endpoint return HttpClient(endpoint, token) -def get_client(): - """HttpClient для ОПЕРАЦИЙ (create/modify/delete/run/poll). - - Если POLYGON_ENDPOINT задан → polygon. - Иначе → автоопределение реального стенда. - """ - return _make_client(use_polygon=True) - - def get_real_client(): - """HttpClient для ЧТЕНИЯ (сервисы, список инстансов). + """Алиас get_client() — историческая совместимость. - ВСЕГДА реальный API, даже при POLYGON_ENDPOINT. - """ - return _make_client(use_polygon=False) + Раньше get_real_client() всегда ходил в реальный API (для сервисов). + Теперь в эмуляции сервисы тоже из полигона → обе функции идентичны. + Оставлено чтобы не ломать все места вызова.""" + return get_client() def get_client_id(): @@ -100,12 +117,13 @@ def get_client_id(): def get_stand(): - """Определить стенд (dev/test/mock) по активному токену. + """Определить стенд. - Если POLYGON_ENDPOINT задан → "polygon". - Иначе — автоопределение или stand_name.""" - if current_app.config.get("POLYGON_ENDPOINT", ""): - return "polygon" + Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера. + Облако: автоопределение или stand_name (dev/test/mock).""" + mode = get_mode() + if mode == "polygon": + return f"polygon_{get_polygon_stand()}" token = get_token() endpoint = current_app.config["NUBES_API_ENDPOINT"] diff --git a/site/app.py b/site/app.py index 30f5ffa..d694a57 100644 --- a/site/app.py +++ b/site/app.py @@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp # Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода. # Нужна для фильтрации истории (пользователь видит только записи своей версии). -VERSION = "1.2.29" +VERSION = "1.2.30" # Flask-приложение с Jinja2-шаблонами из папки templates/ app = Flask(__name__, template_folder="templates", static_folder="static") diff --git a/site/operations/service_list.py b/site/operations/service_list.py index 7999785..5aa0086 100644 --- a/site/operations/service_list.py +++ b/site/operations/service_list.py @@ -33,10 +33,13 @@ def load_service_ids(stand="test"): """Вернуть set разрешённых service_id для указанного стенда. Args: - stand: str — "dev" или "test". + stand: str — "dev", "test", или "polygon_test" (с префиксом эмуляции). Returns: set[int] — множество service_id. Пустой set если файла нет → показываем все.""" + # Обрезать префикс "polygon_" если есть → "polygon_test" → "test" + if stand.startswith("polygon_"): + stand = stand[len("polygon_"):] path = os.path.join(_CONFIG_DIR, f"services_{stand}.txt") ids = set() try: diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 2f0fcb8..f7d9f6e 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -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) diff --git a/site/routes/main.py b/site/routes/main.py index df9e81a..27a0b37 100644 --- a/site/routes/main.py +++ b/site/routes/main.py @@ -11,7 +11,7 @@ GET /api/operations/ — операции и 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 = [] diff --git a/site/templates/index.html b/site/templates/index.html index 7a10402..d7ef667 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -59,11 +59,27 @@