92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""Inventory client: per-resource detail from Nova, Cinder, Neutron, Keystone.
|
|
|
|
Endpoints come from the Keystone token catalog (public interface), so this
|
|
works both inside the cluster and externally (console.connectvm.com).
|
|
"""
|
|
import httpx
|
|
|
|
from cloudkitty_client import KeystoneAuth
|
|
from config import Settings
|
|
|
|
NOVA_MICROVERSION = "2.88" # embedded flavor objects in server details
|
|
|
|
|
|
class OpenStackClient:
|
|
def __init__(self, settings: Settings):
|
|
self._s = settings
|
|
self._auth = KeystoneAuth(settings)
|
|
self._client = httpx.AsyncClient(
|
|
verify=settings.verify_ssl, timeout=settings.http_timeout
|
|
)
|
|
self._catalog: list | None = None
|
|
self._project_id: str | None = None
|
|
|
|
async def close(self):
|
|
await self._client.aclose()
|
|
|
|
async def _ensure_catalog(self):
|
|
if self._catalog is None:
|
|
body = await self._auth.token_body()
|
|
token = body.get("token", {})
|
|
self._catalog = token.get("catalog", [])
|
|
self._project_id = token.get("project", {}).get("id")
|
|
|
|
def _endpoint(self, service_type: str, override: str = "") -> str:
|
|
if override:
|
|
return override.rstrip("/")
|
|
for svc in self._catalog or []:
|
|
if svc.get("type") == service_type:
|
|
for ep in svc.get("endpoints", []):
|
|
if ep.get("interface") == "public":
|
|
return ep["url"].rstrip("/")
|
|
raise RuntimeError(f"no public endpoint for service type '{service_type}'")
|
|
|
|
async def _get(self, url: str, params: dict | None = None, headers: dict | None = None):
|
|
token = await self._auth.token()
|
|
h = {"X-Auth-Token": token}
|
|
h.update(headers or {})
|
|
r = await self._client.get(url, params=params, headers=h)
|
|
if r.status_code == 401:
|
|
h["X-Auth-Token"] = await self._auth.token(force=True)
|
|
r = await self._client.get(url, params=params, headers=h)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
async def projects(self) -> dict[str, str]:
|
|
"""Map of project name -> project id."""
|
|
data = await self._get(f"{self._s.keystone_auth_url}/v3/projects")
|
|
return {p["name"]: p["id"] for p in data.get("projects", [])}
|
|
|
|
async def servers(self, project_id: str) -> list[dict]:
|
|
await self._ensure_catalog()
|
|
base = self._endpoint("compute", self._s.nova_url)
|
|
data = await self._get(
|
|
f"{base}/servers/detail",
|
|
params={"all_tenants": 1, "project_id": project_id, "limit": 1000},
|
|
headers={"X-OpenStack-Nova-API-Version": NOVA_MICROVERSION},
|
|
)
|
|
return data.get("servers", [])
|
|
|
|
async def volumes(self, project_id: str) -> list[dict]:
|
|
await self._ensure_catalog()
|
|
base = self._endpoint("volumev3", self._s.cinder_url).replace(
|
|
"%(project_id)s", self._project_id or ""
|
|
)
|
|
data = await self._get(
|
|
f"{base}/volumes/detail",
|
|
params={"all_tenants": 1, "limit": 1000},
|
|
)
|
|
return [
|
|
v
|
|
for v in data.get("volumes", [])
|
|
if v.get("os-vol-tenant-attr:tenant_id") == project_id
|
|
]
|
|
|
|
async def floatingips(self, project_id: str) -> list[dict]:
|
|
await self._ensure_catalog()
|
|
base = self._endpoint("network", self._s.neutron_url)
|
|
data = await self._get(
|
|
f"{base}/v2.0/floatingips", params={"tenant_id": project_id}
|
|
)
|
|
return data.get("floatingips", [])
|