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 @@
"""Test package."""
+1
View File
@@ -0,0 +1 @@
"""oVirt Engine API simulator tests."""
+74
View File
@@ -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)
+202
View File
@@ -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
+151
View File
@@ -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)
+203
View File
@@ -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}"
)
+61
View File
@@ -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
+35
View File
@@ -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
+7
View File
@@ -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"