130 lines
5.7 KiB
Python
130 lines
5.7 KiB
Python
"""
|
||
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 <token> (аутентификация)
|
||
- 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
|
||
return result
|
||
|
||
def raw_delete(self, url):
|
||
"""DELETE-запрос к произвольному URL (CMDB API).
|
||
Не добавляет Nubes-эндпоинт — используется для жёсткого удаления инстансов.
|
||
Возвращает True если 2xx, иначе False."""
|
||
try:
|
||
r = self._session.delete(url, timeout=10)
|
||
return r.ok
|
||
except Exception:
|
||
return False
|