""" HTTP-клиент для Nubes API + автоопределение стенда. Класс HttpClient — тонкая обёртка над requests.Session: - Добавляет заголовки: Authorization Bearer, User-Agent (DDoS-Guard) - GET: raise_for_status → .json() - POST: проверка r.ok, извлечение Location-заголовка Функции автостенда: - detect_endpoint(token) — пробует dev→test стенды, возвращает URL - stand_name(endpoint) — "dev" / "test" по URL - create_client(token, fallback) — HttpClient + endpoint (одним вызовом) """ import requests # Список стендов для автоопределения. Порядок важен: dev первый (быстрее). STANDS = [ "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc", ] def detect_endpoint(token): """Пробуем токен против dev и test стендов, возвращаем рабочий URL. Делает GET /instances?pageSize=1 на каждый стенд. Если results != None — стенд рабочий, возвращаем его URL. Если ни один не подошёл — возвращаем None.""" for ep in STANDS: try: c = HttpClient(ep, token) data = c.get("/instances", params={"pageSize": 1, "page": 1}) if data.get("results") is not None: return ep except Exception: continue return None def stand_name(endpoint): """dev/test по URL стенда. Ищет подстроку 'dev' или 'test' в URL. Если не найдено — возвращает '?'.""" for name in ("dev", "test"): if name in (endpoint or ""): return name return "?" def create_client(token, fallback_endpoint=None): """HttpClient с автоопределением стенда по токену. Сначала detect_endpoint(token), если не сработало — fallback_endpoint. Возвращает кортеж (HttpClient, endpoint_url) или None если стенд не определён.""" ep = detect_endpoint(token) or fallback_endpoint if not ep: return None return HttpClient(ep, token), ep class HttpClient: """HTTP-клиент для Nubes REST API. Использует requests.Session для keep-alive соединений. Добавляет обязательные заголовки: - Authorization: Bearer (аутентификация) - User-Agent: Mozilla/5.0 (DDoS-Guard требует НЕ python-requests)""" def __init__(self, endpoint, token): # Убираем trailing slash чтобы потом добавлять "/path" self._endpoint = endpoint.rstrip("/") # Session — переиспользует TCP-соединения между запросами self._session = requests.Session() self._session.headers.update({ "Authorization": f"Bearer {token}", "User-Agent": "Mozilla/5.0", }) def get(self, path, **kwargs): """GET-запрос. Таймаут по умолчанию 10 секунд. Вызывает raise_for_status() — при 4xx/5xx выбрасывает HTTPError. Возвращает распарсенный JSON (dict/list).""" kwargs.setdefault("timeout", 10) r = self._session.get(f"{self._endpoint}{path}", **kwargs) r.raise_for_status() if not r.text or not r.text.strip(): return {} # пустой ответ (напр. validate-cfs успех) return r.json() def post(self, path, data=None, **kwargs): """POST-запрос. Таймаут по умолчанию 30 секунд. Отправляет data как JSON (json=...). При HTTP-ошибке выбрасывает Exception с кодом и телом ответа. Возвращает dict: - Распарсенный JSON (если ответ — валидный JSON-объект) - + ключ "_location" со значением заголовка Location (если есть) Location нужен для получения UID созданного ресурса (instanceUid, opUid).""" kwargs.setdefault("timeout", 30) url = f"{self._endpoint}{path}" # json=... — requests сам сериализует и ставит Content-Type: application/json r = self._session.post(url, json=(data if data is not None else {}), **kwargs) if not r.ok: raise Exception(f"POST {path}: {r.status_code} {r.reason}: {r.text[:200]}") result = {} try: parsed = r.json() if isinstance(parsed, dict): result = parsed except Exception: pass # тело не JSON или не dict — ок, Location всё равно извлечём loc = r.headers.get("Location", "") if loc: # Location бывает вида "./UUID" или "/api/v1/svc/instanceOperations/UUID" result["_location"] = loc # Извлечь UUID и положить в правильное поле ответа parts = loc.rstrip("/").split("/") uid = parts[-1] if uid and uid != "." and len(uid) >= 32: if "/instanceOperations" in path: result["instanceOperationUid"] = uid elif "/instances" in path: result["instanceUid"] = uid return result def raw_delete(self, url): """DELETE-запрос к произвольному URL (CMDB API). Использует отдельную сессию БЕЗ auth-заголовков. Возвращает кортеж (ok: bool, status_code: int).""" try: r = requests.delete(url, timeout=10) return r.ok, r.status_code except Exception as e: return False, str(e)