121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""PLS API client for cluster, per-node discovery, and site-wide config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
|
import httpx
|
|
|
|
from app.config import PLS_AUTH_BACKEND, PLS_BASE_URL, PLS_PASSWORD, PLS_USERNAME, PLS_VERIFY_TLS
|
|
|
|
_token: str | None = None
|
|
|
|
|
|
class PlsRequestError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _base_url_for_host(host: str | None = None) -> str:
|
|
if not host:
|
|
return PLS_BASE_URL.rstrip("/")
|
|
parts = urlsplit(PLS_BASE_URL)
|
|
return urlunsplit((parts.scheme, host, parts.path.rstrip("/"), "", ""))
|
|
|
|
|
|
async def _login(force: bool = False) -> str | None:
|
|
global _token
|
|
if _token and not force:
|
|
return _token
|
|
if not PLS_USERNAME or not PLS_PASSWORD:
|
|
return None
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5, verify=PLS_VERIFY_TLS) as client:
|
|
response = await client.post(
|
|
f"{_base_url_for_host()}/auth/login",
|
|
json={
|
|
"username": PLS_USERNAME,
|
|
"password": PLS_PASSWORD,
|
|
"backend": PLS_AUTH_BACKEND,
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
_token = data.get("access_token")
|
|
return _token
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def _get(path: str, host: str | None = None) -> dict | list | None:
|
|
token = await _login()
|
|
if not token:
|
|
return None
|
|
|
|
url = f"{_base_url_for_host(host)}/{path.lstrip('/')}"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5, verify=PLS_VERIFY_TLS) as client:
|
|
response = await client.get(url, headers={"Authorization": f"Bearer {token}"})
|
|
if response.status_code in {401, 403}:
|
|
refreshed = await _login(force=True)
|
|
if not refreshed:
|
|
return None
|
|
response = await client.get(url, headers={"Authorization": f"Bearer {refreshed}"})
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def _put(path: str, payload: dict, host: str | None = None) -> dict | list | None:
|
|
token = await _login()
|
|
if not token:
|
|
raise PlsRequestError("PLS authentication is not configured or login failed")
|
|
|
|
url = f"{_base_url_for_host(host)}/{path.lstrip('/')}"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8, verify=PLS_VERIFY_TLS) as client:
|
|
response = await client.put(url, headers={"Authorization": f"Bearer {token}"}, json=payload)
|
|
if response.status_code in {401, 403}:
|
|
refreshed = await _login(force=True)
|
|
if not refreshed:
|
|
raise PlsRequestError("PLS token expired and re-login failed")
|
|
response = await client.put(url, headers={"Authorization": f"Bearer {refreshed}"}, json=payload)
|
|
if response.is_error:
|
|
detail = response.text.strip()
|
|
raise PlsRequestError(f"HTTP {response.status_code}: {detail or 'unknown PLS validation error'}")
|
|
return response.json()
|
|
except PlsRequestError:
|
|
raise
|
|
except Exception as exc:
|
|
raise PlsRequestError(str(exc)) from exc
|
|
|
|
|
|
def node_host(node_name: str) -> str:
|
|
return node_name.split("@", 1)[1] if "@" in node_name else node_name
|
|
|
|
|
|
async def get_cluster_status() -> dict | None:
|
|
data = await _get("data_layer/cluster/status")
|
|
return data if isinstance(data, dict) else None
|
|
|
|
|
|
async def get_system_info(host: str | None = None) -> dict | None:
|
|
data = await _get("system/info", host=host)
|
|
return data if isinstance(data, dict) else None
|
|
|
|
|
|
async def get_services(host: str | None = None) -> list[dict]:
|
|
data = await _get("services", host=host)
|
|
return data if isinstance(data, list) else []
|
|
|
|
|
|
async def get_fluentbit_config() -> dict | None:
|
|
data = await _get("fluent-bit/config")
|
|
return data if isinstance(data, dict) else None
|
|
|
|
|
|
async def put_fluentbit_config(config: dict) -> dict | None:
|
|
data = await _put("fluent-bit/config", config)
|
|
return data if isinstance(data, dict) else None
|