Initial release of the oVirt/RHV Engine API simulator.

Stateful FastAPI lab with contract packs, Compose/Helm, Docker Hub release
targets, and Pulumi coverage across all Engine series (GET/POST/PUT/DELETE/HEAD).
This commit is contained in:
2026-07-18 04:49:28 +03:00
commit cbd0adca91
218 changed files with 246804 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Shared package for oVirt lab suites."""
+47
View File
@@ -0,0 +1,47 @@
"""Shared configuration for the Pulumi Engine contract-coverage lab."""
from __future__ import annotations
import os
from dataclasses import dataclass
def _env(name: str, default: str | None = None, *, allow_empty: bool = False) -> str:
value = os.environ.get(name, default)
if value is None or (value == "" and not allow_empty and default is None):
raise RuntimeError(f"required environment variable {name} is not set")
return value if value is not None else ""
@dataclass(frozen=True)
class SuiteConfig:
api_url: str
api_base: str
sso_base: str
user: str
password: str
verify_tls: bool
timeout_seconds: float
contracts_root: str
series_filter: str
methods_filter: str
smoke_only: bool
report_dir: str
@classmethod
def from_env(cls) -> SuiteConfig:
api_url = _env("OVIRT_URL", "https://api-gateway").rstrip("/")
return cls(
api_url=api_url,
api_base=f"{api_url}/ovirt-engine/api",
sso_base=f"{api_url}/ovirt-engine/sso/oauth",
user=_env("OVIRT_USER", "admin@internal"),
password=_env("OVIRT_PASSWORD", "secret"),
verify_tls=_env("OVIRT_VERIFY_TLS", "0") == "1",
timeout_seconds=float(_env("OVIRT_TIMEOUT", "60")),
contracts_root=_env("OVIRT_CONTRACTS_ROOT", "/contracts/ovirt"),
series_filter=_env("OVIRT_SERIES_FILTER", "", allow_empty=True).strip(),
methods_filter=_env("OVIRT_METHODS_FILTER", "", allow_empty=True).strip().upper(),
smoke_only=_env("SMOKE_ONLY", "0") == "1",
report_dir=_env("REPORT_DIR", "/workspace/reports"),
)
+80
View File
@@ -0,0 +1,80 @@
"""httpx client for Engine API + series activation."""
from __future__ import annotations
import base64
from typing import Any
import httpx
from shared.config import SuiteConfig
class OVirtApiError(RuntimeError):
def __init__(self, method: str, path: str, response: httpx.Response) -> None:
super().__init__(f"{method} {path} -> {response.status_code} {response.text[:300]}")
self.method = method
self.path = path
self.status_code = response.status_code
self.response = response
class OVirtClient:
def __init__(self, cfg: SuiteConfig | None = None, *, api_version: str = "4") -> None:
self.cfg = cfg or SuiteConfig.from_env()
self.api_version = api_version
self._client = httpx.Client(verify=self.cfg.verify_tls, timeout=self.cfg.timeout_seconds)
self.token: str | None = None
def __enter__(self) -> OVirtClient:
self.login()
return self
def __exit__(self, *exc: object) -> None:
self.close()
def close(self) -> None:
self._client.close()
def login(self) -> str:
r = self._client.post(
f"{self.cfg.sso_base}/token",
data={
"grant_type": "password",
"username": self.cfg.user,
"password": self.cfg.password,
"scope": "ovirt-app-api",
},
headers={"Accept": "application/json"},
)
if r.status_code != 200:
raise OVirtApiError("POST", "/ovirt-engine/sso/oauth/token", r)
self.token = r.json()["access_token"]
return self.token
def headers(self, *, version: str | None = None) -> dict[str, str]:
if not self.token:
self.login()
return {
"Authorization": f"Bearer {self.token}",
"Accept": "application/json",
"Content-Type": "application/json",
"Version": version or self.api_version,
}
def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
url = path if path.startswith("http") else f"{self.cfg.api_url}{path}"
headers = kwargs.pop("headers", None) or self.headers()
return self._client.request(method, url, headers=headers, **kwargs)
def activate_series(self, series: str) -> httpx.Response:
return self._client.post(
f"{self.cfg.api_url}/ui/api/ovirt/contracts/activate",
json={"series": series},
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
def basic_probe(self) -> None:
r = self._client.get(f"{self.cfg.api_url}/health/ready", headers={"Accept": "application/json"})
if r.status_code != 200:
raise RuntimeError(f"simulator not ready: {r.status_code}")