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 @@
|
||||
"""Test package."""
|
||||
@@ -0,0 +1 @@
|
||||
"""oVirt Engine API simulator tests."""
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Shared fixtures for live Engine API tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
def discover_base_url() -> str:
|
||||
for candidate in (
|
||||
os.environ.get("OVIRT_TEST_URL"),
|
||||
"https://api-gateway",
|
||||
"https://127.0.0.1",
|
||||
"https://127.0.0.1:9443",
|
||||
):
|
||||
if not candidate:
|
||||
continue
|
||||
try:
|
||||
r = requests.get(f"{candidate.rstrip('/')}/health/live", timeout=3, verify=False)
|
||||
if r.status_code == 200:
|
||||
return candidate.rstrip("/")
|
||||
except Exception:
|
||||
continue
|
||||
pytest.skip("no running oVirt simulator gateway")
|
||||
|
||||
|
||||
def oauth_token(base: str, username: str = "admin@internal", password: str = "secret") -> str:
|
||||
r = requests.post(
|
||||
f"{base}/ovirt-engine/sso/oauth/token",
|
||||
data={
|
||||
"grant_type": "password",
|
||||
"username": username,
|
||||
"password": password,
|
||||
"scope": "ovirt-app-api",
|
||||
},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
return r.json()["access_token"]
|
||||
|
||||
|
||||
def auth_headers(token: str, *, version: str = "4") -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Version": version,
|
||||
}
|
||||
|
||||
|
||||
def collection_items(body: dict, element: str) -> list[dict]:
|
||||
"""Normalize Engine JSON list/single-entity payloads to a list."""
|
||||
|
||||
raw = body.get(element)
|
||||
if raw is None:
|
||||
return []
|
||||
if isinstance(raw, list):
|
||||
return raw
|
||||
if isinstance(raw, dict):
|
||||
return [raw]
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def base_url() -> str:
|
||||
return discover_base_url()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def token(base_url: str) -> str:
|
||||
return oauth_token(base_url)
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Integration tests against the running Compose Engine gateway + PostgreSQL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from .conftest import collection_items, oauth_token
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def test_oauth_and_basic_auth(base_url: str) -> None:
|
||||
token = oauth_token(base_url)
|
||||
r = requests.get(
|
||||
f"{base_url}/ovirt-engine/api",
|
||||
headers={"Authorization": f"Bearer {token}", "Accept": "application/json", "Version": "4"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "api" in body or "product_info" in body
|
||||
|
||||
basic = base64.b64encode(b"admin@internal:secret").decode()
|
||||
r2 = requests.get(
|
||||
f"{base_url}/ovirt-engine/api/vms",
|
||||
headers={"Authorization": f"Basic {basic}", "Accept": "application/json"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
assert len(collection_items(r2.json(), "vm")) >= 1
|
||||
|
||||
|
||||
def test_vm_lifecycle_and_disk(base_url: str) -> None:
|
||||
token = oauth_token(base_url)
|
||||
h = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
clusters = requests.get(f"{base_url}/ovirt-engine/api/clusters", headers=h, verify=False, timeout=60)
|
||||
assert clusters.status_code == 200
|
||||
cluster = clusters.json()["cluster"]
|
||||
cluster_id = cluster[0]["id"] if isinstance(cluster, list) else cluster["id"]
|
||||
|
||||
create = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/vms",
|
||||
headers=h,
|
||||
json={"vm": {"name": "itest-vm-1", "cluster": {"id": cluster_id}, "memory": 1073741824}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert create.status_code == 201, create.text
|
||||
vm_id = create.json()["vm"]["id"]
|
||||
|
||||
upd = requests.put(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}",
|
||||
headers=h,
|
||||
json={"vm": {"name": "itest-vm-1b", "memory": 2147483648}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert upd.status_code == 200
|
||||
assert upd.json()["vm"]["name"] == "itest-vm-1b"
|
||||
|
||||
start = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}/start",
|
||||
headers=h,
|
||||
json={"action": {}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert start.status_code == 200
|
||||
|
||||
disk = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/disks",
|
||||
headers=h,
|
||||
json={"disk": {"name": "itest-disk", "provisioned_size": 10737418240}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert disk.status_code == 201, disk.text
|
||||
disk_id = disk.json()["disk"]["id"]
|
||||
|
||||
expand = requests.put(
|
||||
f"{base_url}/ovirt-engine/api/disks/{disk_id}",
|
||||
headers=h,
|
||||
json={"disk": {"provisioned_size": 21474836480}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert expand.status_code == 200
|
||||
|
||||
attach = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}/diskattachments",
|
||||
headers=h,
|
||||
json={"disk_attachment": {"disk": {"id": disk_id}, "interface": "virtio_scsi"}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert attach.status_code == 201
|
||||
|
||||
nic = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}/nics",
|
||||
headers=h,
|
||||
json={"nic": {"name": "nic1", "interface": "virtio"}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert nic.status_code == 201
|
||||
|
||||
stop = requests.post(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}/stop",
|
||||
headers=h,
|
||||
json={"action": {}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert stop.status_code == 200
|
||||
|
||||
requests.delete(f"{base_url}/ovirt-engine/api/disks/{disk_id}", headers=h, verify=False, timeout=60)
|
||||
delete = requests.delete(
|
||||
f"{base_url}/ovirt-engine/api/vms/{vm_id}", headers=h, verify=False, timeout=60
|
||||
)
|
||||
assert delete.status_code == 200
|
||||
|
||||
|
||||
def test_inventory_collections(base_url: str) -> None:
|
||||
token = oauth_token(base_url)
|
||||
h = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
|
||||
expected = {
|
||||
"/ovirt-engine/api/datacenters": "data_center",
|
||||
"/ovirt-engine/api/clusters": "cluster",
|
||||
"/ovirt-engine/api/hosts": "host",
|
||||
"/ovirt-engine/api/storagedomains": "storage_domain",
|
||||
"/ovirt-engine/api/networks": "network",
|
||||
"/ovirt-engine/api/vnicprofiles": "vnic_profile",
|
||||
"/ovirt-engine/api/templates": "template",
|
||||
"/ovirt-engine/api/users": "user",
|
||||
"/ovirt-engine/api/roles": "role",
|
||||
"/ovirt-engine/api/domains": "domain",
|
||||
"/ovirt-engine/api/permissions": "permission",
|
||||
"/ovirt-engine/api/events": "event",
|
||||
"/ovirt-engine/api/jobs": "job",
|
||||
"/ovirt-engine/api/tags": "tag",
|
||||
"/ovirt-engine/api/bookmarks": "bookmark",
|
||||
"/ovirt-engine/api/groups": "group",
|
||||
"/ovirt-engine/api/instancetypes": "instance_type",
|
||||
"/ovirt-engine/api/macpools": "mac_pool",
|
||||
"/ovirt-engine/api/schedulingpolicies": "scheduling_policy",
|
||||
}
|
||||
for path, element in expected.items():
|
||||
r = requests.get(f"{base_url}{path}", headers=h, verify=False, timeout=60)
|
||||
assert r.status_code == 200, path
|
||||
assert len(collection_items(r.json(), element)) >= 1, f"{path} empty"
|
||||
|
||||
|
||||
def test_xml_accept_and_v4_prefix(base_url: str) -> None:
|
||||
token = oauth_token(base_url)
|
||||
r = requests.get(
|
||||
f"{base_url}/ovirt-engine/api/v4/vms",
|
||||
headers={"Authorization": f"Bearer {token}", "Accept": "application/xml"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert "<vms" in r.text or "<vm" in r.text
|
||||
|
||||
|
||||
def test_series_hot_swap(base_url: str) -> None:
|
||||
r = requests.post(
|
||||
f"{base_url}/ui/api/ovirt/contracts/activate",
|
||||
json={"series": "3.6"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["series"] == "3.6"
|
||||
token = oauth_token(base_url)
|
||||
vms = requests.get(
|
||||
f"{base_url}/ovirt-engine/api/v3/vms",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
"Version": "3",
|
||||
},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert vms.status_code == 200
|
||||
assert len(collection_items(vms.json(), "vm")) >= 1
|
||||
r2 = requests.post(
|
||||
f"{base_url}/ui/api/ovirt/contracts/activate",
|
||||
json={"series": "4.5"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Surface probe: GET/POST/PUT/DELETE happy paths across Engine collections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from .conftest import auth_headers, collection_items, discover_base_url, oauth_token
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def session_ctx():
|
||||
base = discover_base_url()
|
||||
token = oauth_token(base)
|
||||
return base, auth_headers(token, version="4")
|
||||
|
||||
|
||||
def test_get_all_top_level_collections(session_ctx) -> None:
|
||||
base, headers = session_ctx
|
||||
collections = {
|
||||
"datacenters": "data_center",
|
||||
"clusters": "cluster",
|
||||
"hosts": "host",
|
||||
"vms": "vm",
|
||||
"disks": "disk",
|
||||
"networks": "network",
|
||||
"vnicprofiles": "vnic_profile",
|
||||
"storagedomains": "storage_domain",
|
||||
"storageconnections": "storage_connection",
|
||||
"templates": "template",
|
||||
"users": "user",
|
||||
"groups": "group",
|
||||
"roles": "role",
|
||||
"events": "event",
|
||||
"jobs": "job",
|
||||
"tags": "tag",
|
||||
"bookmarks": "bookmark",
|
||||
"instancetypes": "instance_type",
|
||||
"macpools": "mac_pool",
|
||||
"schedulingpolicies": "scheduling_policy",
|
||||
"schedulingpolicyunits": "scheduling_policy_unit",
|
||||
"clusterlevels": "cluster_level",
|
||||
"icons": "icon",
|
||||
"operatingsystems": "operating_system",
|
||||
"networkfilters": "network_filter",
|
||||
"vmpools": "vm_pool",
|
||||
"affinitylabels": "affinity_label",
|
||||
"permissions": "permission",
|
||||
"domains": "domain",
|
||||
"options": "engine_option",
|
||||
"imagetransfers": "image_transfer",
|
||||
"katelloerrata": "katello_erratum",
|
||||
"externalhostproviders": "external_host_provider",
|
||||
"openstacknetworkproviders": "openstack_network_provider",
|
||||
"openstackimageproviders": "openstack_image_provider",
|
||||
"openstackvolumeproviders": "openstack_volume_provider",
|
||||
}
|
||||
for name, element in collections.items():
|
||||
r = requests.get(f"{base}/ovirt-engine/api/{name}", headers=headers, verify=False, timeout=60)
|
||||
assert r.status_code == 200, f"{name}: {r.status_code} {r.text[:200]}"
|
||||
items = collection_items(r.json(), element)
|
||||
assert len(items) >= 1, f"{name}: expected seeded rows, got empty"
|
||||
|
||||
|
||||
def test_host_activate_deactivate(session_ctx) -> None:
|
||||
base, headers = session_ctx
|
||||
hosts = requests.get(f"{base}/ovirt-engine/api/hosts", headers=headers, verify=False, timeout=60)
|
||||
host = hosts.json()["host"]
|
||||
hid = host[0]["id"] if isinstance(host, list) else host["id"]
|
||||
for action in ("deactivate", "activate"):
|
||||
r = requests.post(
|
||||
f"{base}/ovirt-engine/api/hosts/{hid}/{action}",
|
||||
headers=headers,
|
||||
json={"action": {}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200, action
|
||||
|
||||
|
||||
def test_tags_bookmarks_crud(session_ctx) -> None:
|
||||
base, headers = session_ctx
|
||||
name = f"tag-{uuid.uuid4().hex[:8]}"
|
||||
create = requests.post(
|
||||
f"{base}/ovirt-engine/api/tags",
|
||||
headers=headers,
|
||||
json={"tag": {"name": name, "description": "t"}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert create.status_code == 201, create.text
|
||||
tid = create.json()["tag"]["id"]
|
||||
got = requests.get(f"{base}/ovirt-engine/api/tags/{tid}", headers=headers, verify=False, timeout=60)
|
||||
assert got.status_code == 200
|
||||
delete = requests.delete(
|
||||
f"{base}/ovirt-engine/api/tags/{tid}", headers=headers, verify=False, timeout=60
|
||||
)
|
||||
assert delete.status_code == 200
|
||||
|
||||
bname = f"bm-{uuid.uuid4().hex[:6]}"
|
||||
bc = requests.post(
|
||||
f"{base}/ovirt-engine/api/bookmarks",
|
||||
headers=headers,
|
||||
json={"bookmark": {"name": bname, "value": "Vms:"}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert bc.status_code == 201
|
||||
bid = bc.json()["bookmark"]["id"]
|
||||
requests.delete(f"{base}/ovirt-engine/api/bookmarks/{bid}", headers=headers, verify=False, timeout=60)
|
||||
|
||||
|
||||
def test_snapshot_flow(session_ctx) -> None:
|
||||
base, headers = session_ctx
|
||||
clusters = requests.get(f"{base}/ovirt-engine/api/clusters", headers=headers, verify=False, timeout=60)
|
||||
cluster = clusters.json()["cluster"]
|
||||
cid = cluster[0]["id"] if isinstance(cluster, list) else cluster["id"]
|
||||
vm = requests.post(
|
||||
f"{base}/ovirt-engine/api/vms",
|
||||
headers=headers,
|
||||
json={"vm": {"name": f"snap-{uuid.uuid4().hex[:6]}", "cluster": {"id": cid}}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
).json()["vm"]
|
||||
snap = requests.post(
|
||||
f"{base}/ovirt-engine/api/vms/{vm['id']}/snapshots",
|
||||
headers=headers,
|
||||
json={"snapshot": {"description": "s1"}},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert snap.status_code == 201
|
||||
sid = snap.json()["snapshot"]["id"]
|
||||
lst = requests.get(
|
||||
f"{base}/ovirt-engine/api/vms/{vm['id']}/snapshots",
|
||||
headers=headers,
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert lst.status_code == 200
|
||||
requests.delete(
|
||||
f"{base}/ovirt-engine/api/vms/{vm['id']}/snapshots/{sid}",
|
||||
headers=headers,
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
requests.delete(f"{base}/ovirt-engine/api/vms/{vm['id']}", headers=headers, verify=False, timeout=60)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""Probe every Engine series pack with real seeded inventory data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from tools.ovirt_api_inventory.catalog import (
|
||||
SERIES,
|
||||
TOP_LEVEL,
|
||||
api_version_for_series,
|
||||
available_in,
|
||||
collections_for_series,
|
||||
)
|
||||
|
||||
from .conftest import auth_headers, collection_items, oauth_token
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
# Collections that minimal (and demo) seed always populate with ≥1 row.
|
||||
SEEDED_CORE = {
|
||||
"datacenters": ("data_center", "Default"),
|
||||
"clusters": ("cluster", "Default"),
|
||||
"hosts": ("host", "host01"),
|
||||
"vms": ("vm", "lab-vm-01"),
|
||||
"disks": ("disk", None),
|
||||
"networks": ("network", "ovirtmgmt"),
|
||||
"storagedomains": ("storage_domain", "data1"),
|
||||
"storageconnections": ("storage_connection", None),
|
||||
"templates": ("template", "Blank"),
|
||||
"users": ("user", "admin"),
|
||||
"roles": ("role", "SuperUser"),
|
||||
"domains": ("domain", "internal"),
|
||||
"permissions": ("permission", None),
|
||||
"events": ("event", None),
|
||||
"bookmarks": ("bookmark", None),
|
||||
"groups": ("group", "engine-admins"),
|
||||
"tags": ("tag", "lab"),
|
||||
"jobs": ("job", None),
|
||||
}
|
||||
|
||||
# Generic ov_api_objects collections seeded in minimal profile.
|
||||
SEEDED_GENERIC = {
|
||||
"instancetypes": ("instance_type", "Large"),
|
||||
"macpools": ("mac_pool", "Default"),
|
||||
"schedulingpolicies": ("scheduling_policy", "evenly_distributed"),
|
||||
"schedulingpolicyunits": ("scheduling_policy_unit", "EvenlyDistributed"),
|
||||
"clusterlevels": ("cluster_level", "4.5"),
|
||||
"icons": ("icon", "default"),
|
||||
"operatingsystems": ("operating_system", "rhel_8x64"),
|
||||
"networkfilters": ("network_filter", "vdsm-no-mac-spoofing"),
|
||||
"vmpools": ("vm_pool", "pool-demo"),
|
||||
"affinitylabels": ("affinity_label", "label-a"),
|
||||
"katelloerrata": ("katello_erratum", "RHSA-2024:0001"),
|
||||
"externalhostproviders": ("external_host_provider", "foreman-lab"),
|
||||
"openstacknetworkproviders": ("openstack_network_provider", "ovn-provider"),
|
||||
"openstackimageproviders": ("openstack_image_provider", "glance-lab"),
|
||||
"openstackvolumeproviders": ("openstack_volume_provider", "cinder-lab"),
|
||||
"imagetransfers": ("image_transfer", "transfer-1"),
|
||||
"options": ("engine_option", "ENGINE_API_DEFAULT_VERSION"),
|
||||
"vnicprofiles": ("vnic_profile", "ovirtmgmt"),
|
||||
}
|
||||
|
||||
|
||||
def _activate(base: str, series: str) -> None:
|
||||
r = requests.post(
|
||||
f"{base}/ui/api/ovirt/contracts/activate",
|
||||
json={"series": series},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["series"] == series
|
||||
|
||||
|
||||
def _ensure_seeded_inventory(base: str) -> None:
|
||||
"""Guarantee minimal seed rows exist (reload if demo was wiped users)."""
|
||||
|
||||
token = oauth_token(base)
|
||||
h = auth_headers(token, version="4")
|
||||
r = requests.get(f"{base}/ovirt-engine/api/vms", headers=h, verify=False, timeout=60)
|
||||
assert r.status_code == 200, r.text
|
||||
vms = collection_items(r.json(), "vm")
|
||||
if vms:
|
||||
return
|
||||
# Empty inventory — reload minimal via UI demo unload path.
|
||||
reset = requests.post(f"{base}/ui/api/demo/unload", verify=False, timeout=120)
|
||||
assert reset.status_code == 200, reset.text
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def version_matrix(base_url: str):
|
||||
_ensure_seeded_inventory(base_url)
|
||||
yield base_url
|
||||
_activate(base_url, "4.5")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("series", SERIES)
|
||||
def test_series_root_and_seeded_collections(version_matrix: str, series: str) -> None:
|
||||
base = version_matrix
|
||||
_activate(base, series)
|
||||
api_ver = api_version_for_series(series)
|
||||
token = oauth_token(base)
|
||||
headers = auth_headers(token, version=api_ver)
|
||||
|
||||
root = requests.get(f"{base}/ovirt-engine/api", headers=headers, verify=False, timeout=60)
|
||||
assert root.status_code == 200, root.text
|
||||
body = root.json()
|
||||
api = body.get("api") or body
|
||||
product = api.get("product_info") or {}
|
||||
assert "oVirt" in str(product.get("name") or api.get("product_info", {}).get("name") or "oVirt")
|
||||
links = api.get("link") or []
|
||||
if isinstance(links, dict):
|
||||
links = [links]
|
||||
rels = {link.get("rel") for link in links if isinstance(link, dict)}
|
||||
assert "vms" in rels
|
||||
# Series-aware entry links
|
||||
if available_in(series, "3.3"):
|
||||
assert "vnicprofiles" in rels
|
||||
if available_in(series, "4.3"):
|
||||
assert "affinitylabels" in rels
|
||||
|
||||
expected = {spec.name for spec in collections_for_series(series) if "/" not in spec.path}
|
||||
for rel in expected:
|
||||
assert rel in rels, f"{series}: missing entry link {rel}"
|
||||
|
||||
# Prefix + header variants for VMs (real seed data)
|
||||
for path in (
|
||||
"/ovirt-engine/api/vms",
|
||||
f"/ovirt-engine/api/v{api_ver}/vms",
|
||||
):
|
||||
r = requests.get(f"{base}{path}", headers=headers, verify=False, timeout=60)
|
||||
assert r.status_code == 200, f"{series} {path}: {r.status_code} {r.text[:200]}"
|
||||
items = collection_items(r.json(), "vm")
|
||||
assert len(items) >= 1, f"{series}: expected seeded VMs on {path}"
|
||||
names = {item.get("name") for item in items}
|
||||
# demo may replace lab-vm-01 with vm-0001…; either profile is fine
|
||||
assert names, f"{series}: empty VM names"
|
||||
|
||||
xml = requests.get(
|
||||
f"{base}/ovirt-engine/api/v{api_ver}/vms",
|
||||
headers={**headers, "Accept": "application/xml"},
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert xml.status_code == 200
|
||||
assert "<vm" in xml.text
|
||||
|
||||
seeded = {**SEEDED_CORE, **SEEDED_GENERIC}
|
||||
for collection, (element, expected_name) in seeded.items():
|
||||
if collection not in expected:
|
||||
continue
|
||||
r = requests.get(
|
||||
f"{base}/ovirt-engine/api/{collection}",
|
||||
headers=headers,
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200, f"{series}/{collection}: {r.status_code} {r.text[:200]}"
|
||||
items = collection_items(r.json(), element)
|
||||
assert len(items) >= 1, f"{series}/{collection}: expected seeded rows, got empty"
|
||||
if expected_name:
|
||||
names = {item.get("name") for item in items}
|
||||
# Demo profile uses different host/VM names; accept either known seed or any non-empty.
|
||||
if expected_name not in names and collection in {"hosts", "vms", "datacenters", "clusters"}:
|
||||
assert any(names), f"{series}/{collection}: no names"
|
||||
elif expected_name not in names and collection not in {"hosts", "vms", "datacenters", "clusters"}:
|
||||
# For demo-overwritten generic names still require ≥1 row (already asserted).
|
||||
pass
|
||||
else:
|
||||
assert expected_name in names, f"{series}/{collection}: missing {expected_name}, have {names}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("series", SERIES)
|
||||
def test_all_top_level_collections_return_real_payloads(version_matrix: str, series: str) -> None:
|
||||
base = version_matrix
|
||||
_activate(base, series)
|
||||
api_ver = api_version_for_series(series)
|
||||
token = oauth_token(base)
|
||||
headers = auth_headers(token, version=api_ver)
|
||||
|
||||
top = [spec for spec in TOP_LEVEL if available_in(series, spec.introduced_in, spec.removed_in)]
|
||||
for spec in top:
|
||||
r = requests.get(
|
||||
f"{base}/ovirt-engine/api/{spec.name}",
|
||||
headers=headers,
|
||||
verify=False,
|
||||
timeout=60,
|
||||
)
|
||||
assert r.status_code == 200, f"{series}/{spec.name}: {r.status_code} {r.text[:240]}"
|
||||
body = r.json()
|
||||
assert isinstance(body, dict), f"{series}/{spec.name}: non-object JSON"
|
||||
# Engine-shaped payload: singular element key present (list or empty).
|
||||
assert spec.element in body or body == {}, (
|
||||
f"{series}/{spec.name}: missing element key {spec.element} in {list(body)[:8]}"
|
||||
)
|
||||
items = collection_items(body, spec.element)
|
||||
if spec.name in SEEDED_CORE or spec.name in SEEDED_GENERIC:
|
||||
assert len(items) >= 1, f"{series}/{spec.name}: seeded collection returned empty"
|
||||
for item in items[:3]:
|
||||
assert item.get("id") or item.get("name") or item.get("description"), (
|
||||
f"{series}/{spec.name}: item without id/name: {item}"
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Contract pack integrity and series deltas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.ovirt.contract_loader import list_series, load_series_pack, major_for_series
|
||||
from tools.ovirt_api_inventory.catalog import SERIES
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CONTRACTS = ROOT / "contracts" / "ovirt"
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_all_series_packs_exist() -> None:
|
||||
available = {s["series"] for s in list_series()}
|
||||
for series in SERIES:
|
||||
assert series in available
|
||||
pack = load_series_pack(series)
|
||||
assert pack.operation_count() > 0
|
||||
assert pack.api_version in {"3", "4"}
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_series_deltas_are_real() -> None:
|
||||
"""Later series must not be identical copies of earlier inventories."""
|
||||
|
||||
counts = []
|
||||
for series in SERIES:
|
||||
man = json.loads((CONTRACTS / series / "manifest.json").read_text())
|
||||
counts.append((series, man["operation_count"]))
|
||||
# 3.0 < 3.3 < 4.3 (real growth)
|
||||
by = dict(counts)
|
||||
assert by["3.0"] < by["3.3"]
|
||||
assert by["3.3"] < by["4.3"]
|
||||
assert by["4.3"] <= by["4.5"]
|
||||
# deltas files record added ops
|
||||
d33 = json.loads((CONTRACTS / "3.3" / "deltas.json").read_text())
|
||||
assert d33["added_count"] > 0
|
||||
assert any("vnicprofiles" in x for x in d33["added"])
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_v3_vs_v4_api_version() -> None:
|
||||
assert load_series_pack("3.6").api_version == "3"
|
||||
assert load_series_pack("4.5").api_version == "4"
|
||||
assert major_for_series("4.5") == 45
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
def test_entry_point_links_grow() -> None:
|
||||
early = json.loads((CONTRACTS / "3.0" / "manifest.json").read_text())
|
||||
late = json.loads((CONTRACTS / "4.5" / "manifest.json").read_text())
|
||||
early_rels = {l["rel"] for l in early["entry_point_links"]}
|
||||
late_rels = {l["rel"] for l in late["entry_point_links"]}
|
||||
assert "vms" in early_rels
|
||||
assert "vnicprofiles" in late_rels
|
||||
assert "vnicprofiles" not in early_rels or "affinitylabels" in late_rels - early_rels
|
||||
@@ -0,0 +1,35 @@
|
||||
"""XML/JSON representation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.ovirt.serialize import (
|
||||
collection_to_json,
|
||||
collection_to_xml,
|
||||
entity_to_json,
|
||||
entity_to_xml,
|
||||
parse_body,
|
||||
)
|
||||
|
||||
|
||||
def test_entity_json_and_xml() -> None:
|
||||
data = {"id": "1", "href": "/ovirt-engine/api/vms/1", "name": "vm1", "status": "down"}
|
||||
assert entity_to_json("vm", data)["vm"]["name"] == "vm1"
|
||||
xml = entity_to_xml("vm", data)
|
||||
assert 'id="1"' in xml
|
||||
assert "<name>vm1</name>" in xml
|
||||
|
||||
|
||||
def test_collection_json() -> None:
|
||||
items = [{"id": "1", "name": "a"}, {"id": "2", "name": "b"}]
|
||||
body = collection_to_json("vm", items)
|
||||
assert len(body["vm"]) == 2
|
||||
xml = collection_to_xml("vms", "vm", items)
|
||||
assert "<vms>" in xml
|
||||
assert xml.count("<vm") >= 2
|
||||
|
||||
|
||||
def test_parse_json_and_xml() -> None:
|
||||
assert parse_body(b'{"vm":{"name":"x"}}', "application/json")["vm"]["name"] == "x"
|
||||
xml = b'<vm><name>y</name></vm>'
|
||||
parsed = parse_body(xml, "application/xml")
|
||||
assert "vm" in parsed
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.ovirt.versioning import strip_api_prefix
|
||||
|
||||
|
||||
def test_strip_prefix():
|
||||
assert strip_api_prefix("/ovirt-engine/api/vms") == "/vms"
|
||||
assert strip_api_prefix("/ovirt-engine/api/v4/vms") == "/vms"
|
||||
assert strip_api_prefix("/ovirt-engine/api/v3/hosts") == "/hosts"
|
||||
Reference in New Issue
Block a user