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:
@@ -0,0 +1,174 @@
|
||||
"""Resolve oVirt Engine series contract pack locations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.ovirt.opspec import OperationSpec, SeriesPack
|
||||
|
||||
_SERIES_MAJOR = {
|
||||
"3.0": 30,
|
||||
"3.1": 31,
|
||||
"3.2": 32,
|
||||
"3.3": 33,
|
||||
"3.4": 34,
|
||||
"3.5": 35,
|
||||
"3.6": 36,
|
||||
"4.3": 43,
|
||||
"4.4": 44,
|
||||
"4.5": 45,
|
||||
"master": 50,
|
||||
}
|
||||
_MAJOR_SERIES = {v: k for k, v in _SERIES_MAJOR.items()}
|
||||
|
||||
|
||||
def contracts_root() -> Path:
|
||||
"""Locate ``contracts/ovirt`` for source, wheel, or Docker layouts."""
|
||||
|
||||
env = os.environ.get("OVIRT_CONTRACTS_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
|
||||
here = Path(__file__).resolve()
|
||||
candidates = [
|
||||
here.parents[2] / "contracts" / "ovirt", # repo checkout or site-packages
|
||||
Path("/app/contracts/ovirt"), # runtime image WORKDIR layout
|
||||
Path.cwd() / "contracts" / "ovirt",
|
||||
]
|
||||
for path in candidates:
|
||||
if path.is_dir():
|
||||
return path
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def series_for_major(major: int) -> str:
|
||||
return _MAJOR_SERIES.get(major, "4.5")
|
||||
|
||||
|
||||
def major_for_series(series: str) -> int:
|
||||
return _SERIES_MAJOR.get(series.lower(), 45)
|
||||
|
||||
|
||||
def list_series() -> list[dict[str, Any]]:
|
||||
root = contracts_root()
|
||||
if not root.exists():
|
||||
return []
|
||||
result: list[dict[str, Any]] = []
|
||||
for path in sorted(root.iterdir()):
|
||||
man = path / "manifest.json"
|
||||
if not man.is_file():
|
||||
continue
|
||||
data = json.loads(man.read_text())
|
||||
result.append(
|
||||
{
|
||||
"series": data.get("series", path.name),
|
||||
"major": data.get("major", major_for_series(path.name)),
|
||||
"api_version": data.get("api_version", "4"),
|
||||
"operation_count": data.get("operation_count", 0),
|
||||
"service_count": data.get("service_count", 1),
|
||||
"checksum": data.get("checksum", ""),
|
||||
"generated_at": data.get("generated_at", ""),
|
||||
"deltas": data.get("deltas", {}),
|
||||
"product": data.get("product", {}),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _op_from_dict(raw: dict[str, Any]) -> OperationSpec:
|
||||
return OperationSpec(
|
||||
operation_id=str(raw["operation_id"]),
|
||||
method=raw["method"],
|
||||
path=str(raw["path"]),
|
||||
resource_type=str(raw.get("resource_type") or "object"),
|
||||
collection_key=str(raw.get("collection_key") or "objects"),
|
||||
element=str(raw.get("element") or raw.get("resource_type") or "object"),
|
||||
kind=str(raw.get("kind") or "collection"),
|
||||
search=bool(raw.get("search", False)),
|
||||
introduced_in=str(raw.get("introduced_in") or "3.0"),
|
||||
requires_auth=bool(raw.get("requires_auth", True)),
|
||||
status_code=int(raw.get("status_code") or 200),
|
||||
create_status=int(raw.get("create_status") or 201),
|
||||
notes=str(raw.get("notes") or ""),
|
||||
response_fixture=raw.get("response_fixture"),
|
||||
)
|
||||
|
||||
|
||||
def load_series_pack(series: str) -> SeriesPack:
|
||||
series = series.lower()
|
||||
series_dir = contracts_root() / series
|
||||
man_path = series_dir / "manifest.json"
|
||||
api_path = series_dir / "api.json"
|
||||
if not man_path.is_file() or not api_path.is_file():
|
||||
raise FileNotFoundError(f"oVirt contract pack not found: {series_dir}")
|
||||
man = json.loads(man_path.read_text())
|
||||
data = json.loads(api_path.read_text())
|
||||
ops = [_op_from_dict(raw) for raw in data.get("operations") or []]
|
||||
return SeriesPack(
|
||||
series=str(man.get("series", series)),
|
||||
api_version=str(man.get("api_version") or data.get("api_version") or "4"),
|
||||
major=int(man.get("major") or major_for_series(series)),
|
||||
operations=ops,
|
||||
product=dict(man.get("product") or data.get("product") or {}),
|
||||
entry_point_links=list(man.get("entry_point_links") or []),
|
||||
checksum=str(man.get("checksum") or ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContractRuntime:
|
||||
series: str = "4.5"
|
||||
pack: SeriesPack | None = None
|
||||
_lock: threading.RLock = field(default_factory=threading.RLock)
|
||||
|
||||
def reload(self, series: str | None = None) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
target = (series or self.series).lower()
|
||||
self.pack = load_series_pack(target)
|
||||
self.series = target
|
||||
return self.summary()
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
pack = self.pack
|
||||
if pack is None:
|
||||
return {
|
||||
"series": self.series,
|
||||
"operation_count": 0,
|
||||
"major": major_for_series(self.series),
|
||||
}
|
||||
return {
|
||||
"series": pack.series,
|
||||
"major": pack.major,
|
||||
"api_version": pack.api_version,
|
||||
"operation_count": pack.operation_count(),
|
||||
"service_count": 1,
|
||||
"checksum": pack.checksum,
|
||||
"product": pack.product,
|
||||
"deltas": next(
|
||||
(s.get("deltas") for s in list_series() if s["series"] == pack.series),
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
_RUNTIME = ContractRuntime()
|
||||
|
||||
|
||||
def get_runtime() -> ContractRuntime:
|
||||
return _RUNTIME
|
||||
|
||||
|
||||
def ensure_loaded(series: str = "4.5") -> ContractRuntime:
|
||||
rt = get_runtime()
|
||||
if rt.pack is None:
|
||||
try:
|
||||
rt.reload(series)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return rt
|
||||
Reference in New Issue
Block a user