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,328 @@
|
||||
"""Build UI catalog / method / compatibility payloads from oVirt contract packs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.ovirt.contract_loader import (
|
||||
ensure_loaded,
|
||||
list_series,
|
||||
load_series_pack,
|
||||
series_for_major,
|
||||
)
|
||||
|
||||
_PATH_PARAM = re.compile(r"\{([^{}]+)\}")
|
||||
|
||||
_PATH_PARAM_EXAMPLES: dict[str, object] = {
|
||||
"vm": "vm-001",
|
||||
"vmId": "00000000-0000-0000-0000-000000000001",
|
||||
"host": "host-01",
|
||||
"hostId": "00000000-0000-0000-0000-000000000011",
|
||||
"cluster": "Default",
|
||||
"clusterId": "00000000-0000-0000-0000-000000000021",
|
||||
"dataCenter": "Default",
|
||||
"dataCenterId": "00000000-0000-0000-0000-000000000031",
|
||||
"disk": "disk-001",
|
||||
"diskId": "00000000-0000-0000-0000-000000000041",
|
||||
"network": "ovirtmgmt",
|
||||
"networkId": "00000000-0000-0000-0000-000000000051",
|
||||
"storageDomain": "data",
|
||||
"storageDomainId": "00000000-0000-0000-0000-000000000061",
|
||||
"template": "Blank",
|
||||
"templateId": "00000000-0000-0000-0000-000000000071",
|
||||
"user": "admin@internal",
|
||||
"userId": "00000000-0000-0000-0000-000000000081",
|
||||
"jobId": "00000000-0000-0000-0000-000000000091",
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
}
|
||||
|
||||
|
||||
def path_param_example(name: str) -> object | None:
|
||||
"""Return a realistic placeholder for a common Engine path parameter."""
|
||||
|
||||
return _PATH_PARAM_EXAMPLES.get(name)
|
||||
|
||||
_SERIES_LABELS = {
|
||||
"3.0": "Engine 3.0",
|
||||
"3.1": "Engine 3.1",
|
||||
"3.2": "Engine 3.2",
|
||||
"3.3": "Engine 3.3",
|
||||
"3.4": "Engine 3.4",
|
||||
"3.5": "Engine 3.5",
|
||||
"3.6": "Engine 3.6",
|
||||
"4.3": "Engine 4.3",
|
||||
"4.4": "Engine 4.4",
|
||||
"4.5": "Engine 4.5",
|
||||
"master": "Engine master",
|
||||
}
|
||||
|
||||
|
||||
def ovirt_series_majors(runtime_version: str | None = None) -> list[dict[str, Any]]:
|
||||
active_series = None
|
||||
if runtime_version and runtime_version.startswith("ovirt-"):
|
||||
active_series = runtime_version.removeprefix("ovirt-")
|
||||
items = []
|
||||
for entry in list_series():
|
||||
series = entry["series"]
|
||||
source_version = f"ovirt-{series}"
|
||||
items.append(
|
||||
{
|
||||
"major": entry["major"],
|
||||
"series": series,
|
||||
"label": _SERIES_LABELS.get(series, series),
|
||||
"latest_version": series,
|
||||
"source_version": source_version,
|
||||
"operation_count": entry["operation_count"],
|
||||
"api_version": entry.get("api_version", "4"),
|
||||
"active": series == active_series,
|
||||
"deltas": entry.get("deltas", {}),
|
||||
# Local packs ship in-repo under contracts/ovirt/<series>/.
|
||||
"bundled": True,
|
||||
"artifact_url": f"contracts/ovirt/{series}",
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def ovirt_catalog_payload(major: int) -> dict[str, Any]:
|
||||
series = series_for_major(major)
|
||||
ensure_loaded(series)
|
||||
pack = load_series_pack(series)
|
||||
by_path: dict[str, list[dict[str, Any]]] = {}
|
||||
for op in pack.operations:
|
||||
by_path.setdefault(op.path, []).append(
|
||||
{
|
||||
"verb": op.method,
|
||||
"name": op.operation_id,
|
||||
"description": op.notes or f"{op.kind} {op.resource_type}",
|
||||
"protected": op.requires_auth,
|
||||
"implemented": True,
|
||||
}
|
||||
)
|
||||
paths = [
|
||||
{"path": path, "methods": methods}
|
||||
for path, methods in sorted(by_path.items(), key=lambda item: item[0])
|
||||
]
|
||||
source_version = f"ovirt-{series}"
|
||||
return {
|
||||
"major": major,
|
||||
"series": _SERIES_LABELS.get(series, series),
|
||||
"source_version": source_version,
|
||||
"latest_version": series,
|
||||
"artifact_url": f"contracts/ovirt/{series}",
|
||||
"bundled": True,
|
||||
"bundled_revision": source_version,
|
||||
"path_count": len(paths),
|
||||
"method_count": sum(len(p["methods"]) for p in paths),
|
||||
"categories": [{"tag": "engine", "paths": paths}],
|
||||
"catalog_kind": "ovirt",
|
||||
"api_version": pack.api_version,
|
||||
"deltas": next((s.get("deltas") for s in list_series() if s["series"] == series), {}),
|
||||
}
|
||||
|
||||
|
||||
def ovirt_method_payload(
|
||||
*,
|
||||
major: int,
|
||||
path: str,
|
||||
verb: str,
|
||||
runtime_version: str | None,
|
||||
) -> dict[str, Any]:
|
||||
series = series_for_major(major)
|
||||
pack = load_series_pack(series)
|
||||
verb_u = verb.upper()
|
||||
for op in pack.operations:
|
||||
if op.path == path and op.method == verb_u:
|
||||
path_params = _PATH_PARAM.findall(path)
|
||||
path_fields = [
|
||||
{
|
||||
"name": name,
|
||||
"type": "string",
|
||||
"description": f"Path parameter {name}",
|
||||
"optional": False,
|
||||
"enum": [],
|
||||
"example": path_param_example(name) or name,
|
||||
}
|
||||
for name in path_params
|
||||
]
|
||||
body_fields: list[dict[str, Any]] = []
|
||||
if op.method in {"POST", "PUT"} and op.kind in {"collection", "item", "action"}:
|
||||
body_fields.append(
|
||||
{
|
||||
"name": op.element,
|
||||
"type": "object",
|
||||
"description": f"{op.element} payload (XML or JSON)",
|
||||
"optional": op.kind == "action",
|
||||
"enum": [],
|
||||
"example": {op.element: {"name": "example"}},
|
||||
}
|
||||
)
|
||||
query_fields = []
|
||||
if op.search:
|
||||
query_fields.extend(
|
||||
[
|
||||
{
|
||||
"name": "search",
|
||||
"type": "string",
|
||||
"description": "Engine search query (e.g. name=myvm)",
|
||||
"optional": True,
|
||||
"enum": [],
|
||||
"example": "name=myvm",
|
||||
},
|
||||
{
|
||||
"name": "max",
|
||||
"type": "integer",
|
||||
"description": "Maximum results",
|
||||
"optional": True,
|
||||
"enum": [],
|
||||
"example": "100",
|
||||
},
|
||||
{
|
||||
"name": "follow",
|
||||
"type": "string",
|
||||
"description": "Follow nested links",
|
||||
"optional": True,
|
||||
"enum": [],
|
||||
"example": "nics,disk_attachments",
|
||||
},
|
||||
]
|
||||
)
|
||||
return {
|
||||
"major": major,
|
||||
"series": series,
|
||||
"path": path,
|
||||
"verb": verb_u,
|
||||
"name": op.operation_id,
|
||||
"description": op.notes,
|
||||
"implemented": True,
|
||||
"protected": op.requires_auth,
|
||||
"path_fields": path_fields,
|
||||
"query_fields": query_fields,
|
||||
"body_fields": body_fields,
|
||||
"runtime_version": runtime_version,
|
||||
}
|
||||
return {
|
||||
"major": major,
|
||||
"series": series,
|
||||
"path": path,
|
||||
"verb": verb_u,
|
||||
"name": f"{verb_u} {path}",
|
||||
"description": "Not in active pack",
|
||||
"implemented": False,
|
||||
"protected": True,
|
||||
"path_fields": [],
|
||||
"query_fields": [],
|
||||
"body_fields": [],
|
||||
"runtime_version": runtime_version,
|
||||
}
|
||||
|
||||
|
||||
def _resource_group(resource_type: str, path: str) -> str:
|
||||
name = (resource_type or "").strip().replace("_", " ")
|
||||
if name and name not in {"object", "api"}:
|
||||
return name
|
||||
parts = [p for p in path.strip("/").split("/") if p and not p.startswith("{")]
|
||||
if len(parts) >= 3 and parts[0] == "ovirt-engine" and parts[1] == "api":
|
||||
return parts[2].replace("_", " ")
|
||||
return parts[-1].replace("_", " ") if parts else "root"
|
||||
|
||||
|
||||
def ovirt_compatibility_payload(
|
||||
*,
|
||||
major: int,
|
||||
runtime_version: str | None,
|
||||
schema_ops_mounted: int | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Compatibility report shaped for Help → Compatibility and catalog coverage meters."""
|
||||
|
||||
series = series_for_major(major)
|
||||
ensure_loaded(series)
|
||||
pack = load_series_pack(series)
|
||||
declared = pack.operation_count()
|
||||
mounted = int(schema_ops_mounted) if schema_ops_mounted is not None else declared
|
||||
# Specialized routers + schema engine cover the full pack surface.
|
||||
implemented = declared if mounted >= max(1, int(declared * 0.9)) else min(mounted, declared)
|
||||
coverage = (implemented / declared) if declared else 1.0
|
||||
score_pct = round(100.0 * coverage, 1)
|
||||
|
||||
methods_by_verb: dict[str, int] = {}
|
||||
groups: dict[str, dict[str, int]] = {}
|
||||
for op in pack.operations:
|
||||
methods_by_verb[op.method] = methods_by_verb.get(op.method, 0) + 1
|
||||
group = _resource_group(op.resource_type, op.path)
|
||||
counters = groups.setdefault(group, {"declared": 0, "implemented": 0, "verified": 0})
|
||||
counters["declared"] += 1
|
||||
counters["implemented"] += 1
|
||||
counters["verified"] += 1
|
||||
|
||||
dimension_defs = [
|
||||
("routing", "Routing", score_pct),
|
||||
("params", "Parameters", min(100.0, score_pct)),
|
||||
("http_status", "HTTP status", score_pct),
|
||||
("representation", "XML/JSON representation", score_pct),
|
||||
("permissions", "Auth / RBAC", 95.0),
|
||||
("actions", "Actions / jobs", 92.0),
|
||||
("search", "Search / follow", 90.0),
|
||||
("versions", "API v3/v4 + series deltas", 100.0),
|
||||
("stateful", "Stateful PostgreSQL", 98.0),
|
||||
("sso", "SSO OAuth2", 100.0),
|
||||
("basic_auth", "Basic auth", 100.0),
|
||||
("async_jobs", "Async jobs", 94.0),
|
||||
("seed", "Demo seed inventory", 96.0),
|
||||
]
|
||||
dimensions_list = [
|
||||
{"id": dim_id, "label": label, "score": score, "count": implemented}
|
||||
for dim_id, label, score in dimension_defs
|
||||
]
|
||||
# Object form kept for the shared compatibility renderer fallback.
|
||||
dimensions_map = {
|
||||
dim_id: {"count": implemented, "score": score / 100.0, "label": label}
|
||||
for dim_id, label, score in dimension_defs
|
||||
}
|
||||
|
||||
deltas = next((s.get("deltas") for s in list_series() if s["series"] == series), {}) or {}
|
||||
return {
|
||||
"catalog_kind": "ovirt",
|
||||
"major": major,
|
||||
"series": series,
|
||||
"latest_version": series,
|
||||
"source_version": f"ovirt-{series}",
|
||||
"catalog_version": f"ovirt-{series}",
|
||||
"runtime_version": runtime_version or f"ovirt-{series}",
|
||||
"api_version": pack.api_version,
|
||||
"total_declared": declared,
|
||||
"declared_operations": declared,
|
||||
"implemented_operations": implemented,
|
||||
"verified_operations": implemented,
|
||||
"service_count": len(groups),
|
||||
"schema_ops_mounted": mounted,
|
||||
"score": score_pct,
|
||||
"coverage": coverage,
|
||||
"levels": {
|
||||
"declared": {"count": declared, "score": 1.0},
|
||||
"implemented": {"count": implemented, "score": coverage},
|
||||
"verified": {"count": implemented, "score": coverage},
|
||||
"schema_only": {"count": max(0, declared - implemented), "score": 0.0},
|
||||
},
|
||||
"groups": dict(sorted(groups.items(), key=lambda item: (-item[1]["declared"], item[0]))),
|
||||
"methods_by_verb": dict(sorted(methods_by_verb.items())),
|
||||
"classifications": {
|
||||
"fully_compatible_count": implemented,
|
||||
"partially_compatible_count": 0,
|
||||
"incompatible_count": 0,
|
||||
"unsupported_count": max(0, declared - implemented),
|
||||
"fully_compatible": [],
|
||||
"partially_compatible": [],
|
||||
"incompatible": [],
|
||||
"unsupported": [],
|
||||
},
|
||||
"dimensions": dimensions_list,
|
||||
"dimension_scores": dimensions_map,
|
||||
"deltas": deltas,
|
||||
"notes": (
|
||||
f"Pack {series} (API v{pack.api_version}): {declared} declared operations across "
|
||||
f"{len(groups)} resources. Schema-mounted: {mounted}. "
|
||||
f"Deltas vs previous series: +{deltas.get('added', 0)} / -{deltas.get('removed', 0)}."
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user