Add OpenStack request-body schemas and nested console PARAM sync.

This commit is contained in:
2026-07-18 08:47:38 +03:00
parent cbd0adca91
commit ae297258b1
46 changed files with 42717 additions and 40135 deletions
+268 -90
View File
@@ -1,21 +1,195 @@
"""Large demo datacenter seed (~1000 VMs + full inventory)."""
"""Sized cluster demo seeds: small / large / big (+ demo→large alias)."""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
from asyncpg import Connection
from app.ovirt.ids import stable_id
from app.ovirt.seed import DEMO_PROFILE, clear_ovirt_state, seed_ovirt
from app.ovirt.seed import clear_ovirt_state
from app.security.auth import hash_secret
DEMO_VM_COUNT = 1000
@dataclass(frozen=True)
class ClusterSizeSpec:
"""Topology + inventory density for a demo cluster size."""
name: str
hosts: int
vms: int
datacenters: int
clusters_per_dc: int
hosts_per_cluster: int
networks_per_dc: int
storage_per_dc: int
templates: tuple[str, ...]
tags: tuple[str, ...]
groups: tuple[str, ...]
events: int
jobs: int
bookmarks: tuple[tuple[str, str], ...]
instancetypes: tuple[str, ...]
macpools: tuple[str, ...]
vmpools: tuple[str, ...]
affinitylabels: tuple[str, ...]
katelloerrata: tuple[str, ...]
icons: tuple[str, ...]
operatingsystems: tuple[str, ...]
async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
"""Replace state with a multi-DC demo inventory including ~1000 VMs."""
CLUSTER_SIZES: dict[str, ClusterSizeSpec] = {
"small": ClusterSizeSpec(
name="small",
hosts=3,
vms=50,
datacenters=1,
clusters_per_dc=1,
hosts_per_cluster=3,
networks_per_dc=2,
storage_per_dc=2,
templates=("rhel9-base", "ubuntu2204-base"),
tags=("lab", "web"),
groups=("developers", "readers"),
events=15,
jobs=5,
bookmarks=(("UpVMs", "Vms: status=up"),),
instancetypes=("Small", "Medium"),
macpools=("Default",),
vmpools=("web-pool",),
affinitylabels=("label-a",),
katelloerrata=("RHSA-2024:0001",),
icons=("default",),
operatingsystems=("rhel_9x64", "ubuntu_22_04"),
),
"large": ClusterSizeSpec(
name="large",
hosts=10,
vms=1000,
datacenters=2,
clusters_per_dc=1,
hosts_per_cluster=5,
networks_per_dc=3,
storage_per_dc=3,
templates=("rhel8-base", "rhel9-base", "win2022-base", "ubuntu2204-base"),
tags=("production", "web", "database", "batch", "gpu"),
groups=("developers", "operators", "readers"),
events=50,
jobs=20,
bookmarks=(
("UpVMs", "Vms: status=up"),
("DownVMs", "Vms: status=down"),
),
instancetypes=("Tiny", "Small", "Medium", "Large", "XLarge"),
macpools=("Default", "Secondary"),
vmpools=("web-pool", "batch-pool"),
affinitylabels=("label-a", "label-b"),
katelloerrata=("RHSA-2024:0001", "RHBA-2024:0002"),
icons=("default", "custom"),
operatingsystems=("rhel_8x64", "rhel_9x64", "windows_2022", "ubuntu_22_04"),
),
"big": ClusterSizeSpec(
name="big",
hosts=30,
vms=2000,
datacenters=3,
clusters_per_dc=2,
hosts_per_cluster=5,
networks_per_dc=3,
storage_per_dc=4,
templates=(
"rhel8-base",
"rhel9-base",
"win2022-base",
"ubuntu2204-base",
"centos-stream9",
"debian12-base",
),
tags=(
"production",
"web",
"database",
"batch",
"gpu",
"edge",
"staging",
"critical",
),
groups=("developers", "operators", "readers", "auditors"),
events=120,
jobs=40,
bookmarks=(
("UpVMs", "Vms: status=up"),
("DownVMs", "Vms: status=down"),
("ProdHosts", "Hosts:"),
),
instancetypes=("Tiny", "Small", "Medium", "Large", "XLarge", "2XLarge", "4XLarge"),
macpools=("Default", "Secondary", "Edge"),
vmpools=("web-pool", "batch-pool", "gpu-pool", "edge-pool"),
affinitylabels=("label-a", "label-b", "label-c", "label-d"),
katelloerrata=("RHSA-2024:0001", "RHBA-2024:0002", "RHSA-2024:1001"),
icons=("default", "custom", "windows", "linux"),
operatingsystems=(
"rhel_8x64",
"rhel_9x64",
"windows_2022",
"ubuntu_22_04",
"centos_stream9",
"debian_12",
),
),
}
# Canonical demo profile names that must not be wiped on simulator restart.
DEMO_PROFILES: frozenset[str] = frozenset(CLUSTER_SIZES) | {"demo"}
# Default / legacy alias target.
DEMO_PROFILE = "large"
DEMO_VM_COUNT = CLUSTER_SIZES["large"].vms
def normalize_cluster_size(size: str | None) -> str:
"""Map CLI/UI aliases to a ClusterSizeSpec name."""
key = (size or DEMO_PROFILE).strip().lower()
if key == "demo":
return "large"
if key not in CLUSTER_SIZES:
raise ValueError(f"unknown cluster size {size!r}; expected small|large|big|demo")
return key
def cluster_size_spec(size: str | None = None) -> ClusterSizeSpec:
return CLUSTER_SIZES[normalize_cluster_size(size)]
_DC_NAMES = (
("dc-prod", "Production", False, 4, 5),
("dc-stage", "Staging", False, 4, 4),
("dc-edge", "Edge", True, 4, 3),
)
_NETWORK_SPECS = (("ovirtmgmt", None), ("vm-net", 100), ("storage-net", 200))
_STORAGE_TYPES = ("nfs", "iscsi", "fcp", "localfs")
_TEMPLATE_SPECS: dict[str, tuple[int, int]] = {
"rhel8-base": (4 * 1024**3, 2),
"rhel9-base": (4 * 1024**3, 2),
"win2022-base": (8 * 1024**3, 4),
"ubuntu2204-base": (2 * 1024**3, 2),
"centos-stream9": (4 * 1024**3, 2),
"debian12-base": (2 * 1024**3, 2),
}
async def seed_ovirt_demo(conn: Connection, size: str | None = None) -> dict[str, Any]:
"""Replace state with a sized multi-host demo inventory."""
spec = cluster_size_spec(size)
expected_hosts = spec.datacenters * spec.clusters_per_dc * spec.hosts_per_cluster
if expected_hosts != spec.hosts:
raise RuntimeError(
f"cluster size {spec.name}: topology hosts {expected_hosts} != declared {spec.hosts}"
)
await clear_ovirt_state(conn)
@@ -39,7 +213,7 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
admin,
)
users = {}
users: dict[str, Any] = {}
for uname, role in (
("admin", role_super),
("ops", role_cluster),
@@ -70,7 +244,7 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
stable_id("group", "engine-admins"),
domain_id,
)
for gname in ("developers", "operators", "readers"):
for gname in spec.groups:
await conn.execute(
"INSERT INTO ov_groups(id, domain_id, name) VALUES($1,$2,$3)",
stable_id("group", gname),
@@ -78,20 +252,14 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
gname,
)
# 3 datacenters, multiple clusters/hosts/storage/networks
dc_specs = [
("dc-prod", "Production", False, 4, 5),
("dc-stage", "Staging", False, 4, 4),
("dc-edge", "Edge", True, 4, 3),
]
dc_specs = list(_DC_NAMES[: spec.datacenters])
clusters: list[tuple[Any, Any, str]] = []
hosts: list[Any] = []
networks: list[Any] = []
profiles: list[Any] = []
storage_domains: list[Any] = []
storage_types = ["nfs", "iscsi", "fcp", "localfs"]
for dc_key, dc_name, local, maj, minor in dc_specs:
for dc_idx, (dc_key, dc_name, local, maj, minor) in enumerate(dc_specs):
dc_id = stable_id("dc", dc_key)
await conn.execute(
"""INSERT INTO ov_datacenters(id, name, description, local, status, version_major, version_minor)
@@ -109,8 +277,8 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
stable_id("quota", dc_key),
dc_id,
)
for ci in range(2):
cname = f"{dc_key}-cluster-{ci+1}"
for ci in range(spec.clusters_per_dc):
cname = f"{dc_key}-cluster-{ci + 1}"
cid = stable_id("cluster", cname)
clusters.append((cid, dc_id, cname))
await conn.execute(
@@ -119,7 +287,7 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
cid,
dc_id,
cname,
f"Cluster {ci+1} in {dc_name}",
f"Cluster {ci + 1} in {dc_name}",
maj,
minor,
)
@@ -129,8 +297,8 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
stable_id("ag", cname),
cid,
)
for hi in range(4):
hname = f"{cname}-host-{hi+1:02d}"
for hi in range(spec.hosts_per_cluster):
hname = f"{cname}-host-{hi + 1:02d}"
hid = stable_id("host", hname)
hosts.append(hid)
await conn.execute(
@@ -139,19 +307,27 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
hid,
cid,
hname,
f"10.{dc_specs.index((dc_key, dc_name, local, maj, minor))+10}.{ci+1}.{hi+10}",
f"10.{10 + dc_idx}.{ci + 1}.{hi + 10}",
(256 + hi * 32) * 1024**3,
)
# networks
for nname, vlan in (("ovirtmgmt", None), ("vm-net", 100), ("storage-net", 200)):
for nname, vlan in _NETWORK_SPECS[: spec.networks_per_dc]:
nid = stable_id("net", dc_key, nname)
networks.append(nid)
if nname == "ovirtmgmt" and dc_key == "dc-prod":
net_label = "ovirtmgmt"
elif nname == "ovirtmgmt":
net_label = f"{dc_key}-ovirtmgmt"
elif spec.datacenters > 1:
net_label = f"{dc_key}-{nname}"
else:
net_label = nname
await conn.execute(
"""INSERT INTO ov_networks(id, datacenter_id, name, description, vlan_id)
VALUES($1,$2,$3,$4,$5)""",
nid,
dc_id,
nname if nname != "ovirtmgmt" else f"{dc_key}-ovirtmgmt" if dc_key != "dc-prod" else "ovirtmgmt",
net_label,
f"{nname} in {dc_name}",
vlan,
)
@@ -163,9 +339,9 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
nid,
nname,
)
# storage domains
for si, stype in enumerate(storage_types):
sname = f"{dc_key}-{stype}-{si+1}"
for si, stype in enumerate(_STORAGE_TYPES[: spec.storage_per_dc]):
sname = f"{dc_key}-{stype}-{si + 1}"
sid = stable_id("sd", sname)
storage_domains.append(sid)
await conn.execute(
@@ -201,12 +377,8 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
clusters[0][0],
1024**3,
)
for tname, mem, cores in (
("rhel8-base", 4 * 1024**3, 2),
("rhel9-base", 4 * 1024**3, 2),
("win2022-base", 8 * 1024**3, 4),
("ubuntu2204-base", 2 * 1024**3, 2),
):
for tname in spec.templates:
mem, cores = _TEMPLATE_SPECS.get(tname, (2 * 1024**3, 2))
await conn.execute(
"""INSERT INTO ov_templates(id, cluster_id, name, description, status, memory, cpu_sockets, cpu_cores)
VALUES($1,$2,$3,$4,'ok',$5,1,$6)""",
@@ -218,11 +390,9 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
cores,
)
# ~1000 VMs spread across clusters
statuses = ["up", "up", "up", "down", "down", "suspended", "powering_up"]
os_types = ["rhel_8x64", "rhel_9x64", "ubuntu_22_04", "windows_2022", "other"]
os_types = list(spec.operatingsystems) or ["other"]
default_profile = profiles[0]
default_sd = storage_domains[0]
vm_rows = []
disk_rows = []
@@ -230,13 +400,13 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
nic_rows = []
snap_rows = []
for i in range(DEMO_VM_COUNT):
cluster_id, _dc, cname = clusters[i % len(clusters)]
for i in range(spec.vms):
cluster_id, _dc, _cname = clusters[i % len(clusters)]
host_id = hosts[i % len(hosts)] if i % 3 != 0 else None
status = statuses[i % len(statuses)]
if status == "down":
host_id = None
name = f"vm-{i+1:04d}"
name = f"vm-{i + 1:04d}"
vm_id = stable_id("vm", name)
memory = (1 + (i % 8)) * 1024**3
cores = 1 + (i % 8)
@@ -246,7 +416,7 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
cluster_id,
blank_id,
name,
f"Demo VM {i+1}",
f"Demo VM {i + 1}",
status,
memory,
1,
@@ -273,11 +443,8 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
)
)
if i % 7 == 0:
snap_rows.append(
(stable_id("snap", name, "1"), vm_id, f"snapshot-{name}", "ok")
)
snap_rows.append((stable_id("snap", name, "1"), vm_id, f"snapshot-{name}", "ok"))
# Batch insert VMs
await conn.executemany(
"""INSERT INTO ov_vms(id, cluster_id, template_id, name, description, status,
memory, cpu_sockets, cpu_cores, cpu_threads, os_type, type, host_id)
@@ -306,8 +473,8 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
snap_rows,
)
# Tags, bookmarks, events, jobs, surface objects
for tname in ("production", "web", "database", "batch", "gpu"):
tag_step = max(1, spec.vms // 10)
for tname in spec.tags:
tid = stable_id("tag", tname)
await conn.execute(
"INSERT INTO ov_tags(id, name, description) VALUES($1,$2,$3)",
@@ -315,20 +482,23 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
tname,
f"Tag {tname}",
)
for i in range(0, min(50, DEMO_VM_COUNT), 10):
for i in range(0, min(spec.vms, tag_step * 5), tag_step):
await conn.execute(
"""INSERT INTO ov_tag_assignments(id, tag_id, object_type, object_id)
VALUES($1,$2,'vm',$3) ON CONFLICT DO NOTHING""",
stable_id("ta", tname, str(i)),
tid,
stable_id("vm", f"vm-{i+1:04d}"),
stable_id("vm", f"vm-{i + 1:04d}"),
)
await conn.execute(
"INSERT INTO ov_bookmarks(id, name, value) VALUES($1,'UpVMs','Vms: status=up')",
stable_id("bm", "UpVMs"),
)
for i in range(50):
for bname, bvalue in spec.bookmarks:
await conn.execute(
"INSERT INTO ov_bookmarks(id, name, value) VALUES($1,$2,$3)",
stable_id("bm", bname),
bname,
bvalue,
)
for i in range(spec.events):
await conn.execute(
"""INSERT INTO ov_events(code, severity, description, user_id)
VALUES($1,$2,$3,$4)""",
@@ -337,7 +507,7 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
f"Demo event {i}",
users["admin"],
)
for i in range(20):
for i in range(spec.jobs):
jid = stable_id("job", str(i))
await conn.execute(
"""INSERT INTO ov_jobs(id, description, status, owner_id)
@@ -354,24 +524,31 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
f"Step for job {i}",
)
for collection, names in (
("instancetypes", ["Tiny", "Small", "Medium", "Large", "XLarge"]),
("macpools", ["Default", "Secondary"]),
("schedulingpolicies", ["evenly_distributed", "power_saving", "vm_evenly_distributed"]),
("schedulingpolicyunits", ["EvenlyDistributed", "PowerSaving", "VmEvenlyDistributed"]),
("clusterlevels", ["4.3", "4.4", "4.5"]),
("icons", ["default", "custom"]),
("operatingsystems", ["rhel_8x64", "rhel_9x64", "windows_2022", "ubuntu_22_04"]),
("networkfilters", ["vdsm-no-mac-spoofing"]),
("vmpools", ["web-pool", "batch-pool"]),
("affinitylabels", ["label-a", "label-b"]),
("katelloerrata", ["RHSA-2024:0001", "RHBA-2024:0002"]),
("externalhostproviders", ["foreman-lab"]),
("openstacknetworkproviders", ["ovn-provider"]),
("openstackimageproviders", ["glance-lab"]),
("openstackvolumeproviders", ["cinder-lab"]),
("imagetransfers", ["transfer-1"]),
):
surface: list[tuple[str, tuple[str, ...]]] = [
("instancetypes", spec.instancetypes),
("macpools", spec.macpools),
(
"schedulingpolicies",
("evenly_distributed", "power_saving", "vm_evenly_distributed"),
),
(
"schedulingpolicyunits",
("EvenlyDistributed", "PowerSaving", "VmEvenlyDistributed"),
),
("clusterlevels", ("4.3", "4.4", "4.5")),
("icons", spec.icons),
("operatingsystems", spec.operatingsystems),
("networkfilters", ("vdsm-no-mac-spoofing",)),
("vmpools", spec.vmpools),
("affinitylabels", spec.affinitylabels),
("katelloerrata", spec.katelloerrata),
("externalhostproviders", ("foreman-lab",)),
("openstacknetworkproviders", ("ovn-provider",)),
("openstackimageproviders", ("glance-lab",)),
("openstackvolumeproviders", ("cinder-lab",)),
("imagetransfers", ("transfer-1",)),
]
for collection, names in surface:
for name in names:
await conn.execute(
"""INSERT INTO ov_api_objects(id, collection, name, status, data)
@@ -389,41 +566,34 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
from app.ovirt.seed_nested import seed_nested_for_inventory
dc_ids = [stable_id("dc", key) for key, *_ in dc_specs]
cluster_ids = [c[0] for c in clusters]
template_ids = [
blank_id,
*[
stable_id("template", n)
for n in ("rhel8-base", "rhel9-base", "win2022-base", "ubuntu2204-base")
],
]
tag_ids = [stable_id("tag", t) for t in ("production", "web", "database", "batch", "gpu")]
template_ids = [blank_id, *[stable_id("template", n) for n in spec.templates]]
tag_ids = [stable_id("tag", t) for t in spec.tags]
await seed_nested_for_inventory(
conn,
admin_user_id=users["admin"],
role_user_id=role_user,
datacenter_ids=dc_ids,
cluster_ids=cluster_ids,
cluster_ids=[c[0] for c in clusters],
host_ids=list(hosts),
network_ids=list(networks),
storage_domain_ids=list(storage_domains),
template_ids=template_ids,
vm_ids=[stable_id("vm", f"vm-{i:04d}") for i in range(1, DEMO_VM_COUNT + 1)],
disk_ids=[stable_id("disk", f"vm-{i:04d}") for i in range(1, DEMO_VM_COUNT + 1)],
vm_ids=[stable_id("vm", f"vm-{i:04d}") for i in range(1, spec.vms + 1)],
disk_ids=[stable_id("disk", f"vm-{i:04d}") for i in range(1, spec.vms + 1)],
tag_ids=tag_ids,
user_ids=list(users.values()),
group_ids=[
stable_id("group", n)
for n in ("engine-admins", "developers", "operators", "readers")
for n in ("engine-admins", *spec.groups)
],
)
await conn.execute(
"INSERT INTO ov_demo_meta(key, value) VALUES('profile', $1)", DEMO_PROFILE
"INSERT INTO ov_demo_meta(key, value) VALUES('profile', $1)", spec.name
)
return {
"profile": DEMO_PROFILE,
"vms": DEMO_VM_COUNT,
"profile": spec.name,
"vms": spec.vms,
"hosts": len(hosts),
"datacenters": len(dc_specs),
"clusters": len(clusters),
@@ -432,5 +602,13 @@ async def seed_ovirt_demo(conn: Connection) -> dict[str, Any]:
}
# Re-export for web routes
__all__ = ["DEMO_PROFILE", "DEMO_VM_COUNT", "clear_ovirt_state", "seed_ovirt", "seed_ovirt_demo"]
__all__ = [
"CLUSTER_SIZES",
"DEMO_PROFILE",
"DEMO_PROFILES",
"DEMO_VM_COUNT",
"clear_ovirt_state",
"cluster_size_spec",
"normalize_cluster_size",
"seed_ovirt_demo",
]
+425 -49
View File
@@ -183,6 +183,18 @@ async def handle_engine_request(request: Request) -> Response:
"DataError",
"InvalidTextRepresentationError",
} or "invalid input syntax for type uuid" in detail:
if name == "UniqueViolationError" or "duplicate key" in detail.lower():
raise OVirtError(
"OperationFailed",
"Entity already exists",
status_code=409,
) from exc
if name == "ForeignKeyViolationError":
raise OVirtError(
"BadRequest",
"Referenced entity does not exist",
status_code=400,
) from exc
raise OVirtError("BadRequest", detail or name, status_code=400) from exc
raise
@@ -354,6 +366,12 @@ async def _dispatch(
payload,
)
# Convenience top-level lists (pack ops are nested; avoid empty schema fallbacks).
if parts[0] == "affinitygroups":
return await _handle_top_affinity_groups(request, conn, method, parts)
if parts[0] == "quotas":
return await _handle_top_quotas(request, conn, method, parts)
# Fall through to schema/generic object store
from app.ovirt.schema_engine import handle_generic
@@ -383,13 +401,39 @@ async def _handle_vms(
body = unwrap_entity(payload, "vm")
name = str(body.get("name") or f"vm-{uuid4().hex[:8]}")
cluster = body.get("cluster") or {}
cluster_id = cluster.get("id") if isinstance(cluster, dict) else None
cluster_id = None
if isinstance(cluster, dict):
cluster_id = cluster.get("id")
if cluster_id:
exists = await conn.fetchval(
"SELECT 1 FROM ov_clusters WHERE id=$1::uuid", cluster_id
)
if not exists:
cluster_id = None
if not cluster_id and cluster.get("name"):
cluster_id = await conn.fetchval(
"SELECT id FROM ov_clusters WHERE name=$1 LIMIT 1",
str(cluster["name"]),
)
if not cluster_id:
cluster_id = await conn.fetchval("SELECT id FROM ov_clusters ORDER BY name LIMIT 1")
if not cluster_id:
raise OVirtError("BadRequest", "cluster is required", status_code=400)
template = body.get("template") or {}
template_id = template.get("id") if isinstance(template, dict) else None
template_id = None
if isinstance(template, dict):
template_id = template.get("id")
if template_id:
exists = await conn.fetchval(
"SELECT 1 FROM ov_templates WHERE id=$1::uuid", template_id
)
if not exists:
template_id = None
if not template_id and template.get("name"):
template_id = await conn.fetchval(
"SELECT id FROM ov_templates WHERE name=$1 LIMIT 1",
str(template["name"]),
)
if not template_id:
template_id = await conn.fetchval(
"SELECT id FROM ov_templates WHERE name='Blank' LIMIT 1"
@@ -516,7 +560,20 @@ async def _handle_vms(
raise OVirtError("NotFound", f"VM {vm_id} not found", status_code=404)
if action == "clone":
body = unwrap_entity(payload, "vm") if payload else {}
new_name = str(body.get("name") or f"{row['name']}-clone")
# Action root may wrap `vm: { name }` or place name on the action itself.
nested = body.get("vm") if isinstance(body.get("vm"), dict) else None
new_name = str(
(nested or {}).get("name")
or body.get("name")
or f"{row['name']}-clone"
)
existing = await conn.fetchval("SELECT 1 FROM ov_vms WHERE name=$1", new_name)
if existing:
raise OVirtError(
"OperationFailed",
f"Cannot clone VM. VM name '{new_name}' is already used.",
status_code=409,
)
new_id = uuid4()
await conn.execute(
"""INSERT INTO ov_vms(id, cluster_id, template_id, name, description, status,
@@ -535,6 +592,7 @@ async def _handle_vms(
row["os_type"],
row["type"],
)
await _copy_vm_storage_and_nics(conn, source_vm_id=vm_id, target_vm_id=str(new_id))
return await respond_action(
request, conn, description=f"Clone VM {row['name']}", owner_id=user_id
)
@@ -605,7 +663,25 @@ async def _vm_disk_attachments(
body = unwrap_entity(payload, "disk_attachment")
disk = body.get("disk") or {}
disk_id = disk.get("id") if isinstance(disk, dict) else None
if not disk_id:
if disk_id:
disk_row = await conn.fetchrow(
"SELECT id FROM ov_disks WHERE id=$1::uuid", disk_id
)
if disk_row is None:
raise OVirtError("NotFound", f"Disk {disk_id} not found", status_code=404)
already = await conn.fetchval(
"""SELECT 1 FROM ov_disk_attachments
WHERE vm_id=$1::uuid AND disk_id=$2::uuid""",
vm_id,
disk_id,
)
if already:
raise OVirtError(
"OperationFailed",
"Cannot attach Disk. Disk is already attached to this VM.",
status_code=409,
)
else:
size = int(
(disk or {}).get("provisioned_size")
or await option_int(conn, OPT_DEFAULT_DISK_SIZE)
@@ -695,6 +771,13 @@ async def _vm_nics(
)
if len(parts) >= 4:
nic_id = parts[3]
if len(parts) == 4 and method == "GET":
r = await conn.fetchrow(
"SELECT * FROM ov_nics WHERE id=$1::uuid AND vm_id=$2::uuid", nic_id, vm_id
)
if r is None:
raise OVirtError("NotFound", "nic not found", status_code=404)
return respond(request, element="nic", data=nic_entity(r, vm_id=vm_id))
if len(parts) == 4 and method == "DELETE":
await conn.execute(
"DELETE FROM ov_nics WHERE id=$1::uuid AND vm_id=$2::uuid", nic_id, vm_id
@@ -712,9 +795,18 @@ async def _vm_nics(
profile_id,
body.get("name"),
)
elif body.get("name"):
await conn.execute(
"UPDATE ov_nics SET name=$3 WHERE id=$1::uuid AND vm_id=$2::uuid",
nic_id,
vm_id,
body.get("name"),
)
r = await conn.fetchrow(
"SELECT * FROM ov_nics WHERE id=$1::uuid AND vm_id=$2::uuid", nic_id, vm_id
)
if r is None:
raise OVirtError("NotFound", "nic not found", status_code=404)
return respond(request, element="nic", data=nic_entity(r, vm_id=vm_id))
if len(parts) == 5 and method == "POST" and parts[4] in {"activate", "deactivate"}:
plugged = parts[4] == "activate"
@@ -763,6 +855,13 @@ async def _vm_snapshots(
return respond(
request, element="snapshot", data=snapshot_entity(row, vm_id=vm_id), status_code=201
)
if len(parts) == 4 and method == "GET":
row = await conn.fetchrow(
"SELECT * FROM ov_snapshots WHERE id=$1::uuid AND vm_id=$2::uuid", parts[3], vm_id
)
if row is None:
raise OVirtError("NotFound", "snapshot not found", status_code=404)
return respond(request, element="snapshot", data=snapshot_entity(row, vm_id=vm_id))
if len(parts) == 4 and method == "DELETE":
await conn.execute(
"DELETE FROM ov_snapshots WHERE id=$1::uuid AND vm_id=$2::uuid", parts[3], vm_id
@@ -788,6 +887,17 @@ async def _vm_tags(
)
items = [tag_entity(r) for r in rows]
return respond(request, element="tag", collection="tags", data=items)
if len(parts) == 4 and method == "GET":
row = await conn.fetchrow(
"""SELECT t.* FROM ov_tags t
JOIN ov_tag_assignments a ON a.tag_id=t.id
WHERE a.object_type='vm' AND a.object_id=$1::uuid AND t.id=$2::uuid""",
vm_id,
parts[3],
)
if row is None:
raise OVirtError("NotFound", "tag not found on vm", status_code=404)
return respond(request, element="tag", data=tag_entity(row))
if len(parts) == 3 and method == "POST":
body = unwrap_entity(payload, "tag")
tag_id = body.get("id")
@@ -1049,16 +1159,25 @@ async def _handle_datacenters(
rows = await conn.fetch(
"SELECT * FROM ov_quotas WHERE datacenter_id=$1::uuid ORDER BY name", dc_id
)
items = [
{
"id": str(r["id"]),
"href": f"/ovirt-engine/api/datacenters/{dc_id}/quotas/{r['id']}",
"name": r["name"],
"description": r["description"],
}
for r in rows
]
items = [_quota_entity(r, dc_id) for r in rows]
return respond(request, element="quota", collection="quotas", data=items)
if method == "POST" and len(parts) == 3:
body = unwrap_entity(payload, "quota")
qid = uuid4()
await conn.execute(
"""INSERT INTO ov_quotas(id, datacenter_id, name, description)
VALUES($1,$2::uuid,$3,$4)""",
qid,
dc_id,
str(body.get("name") or f"quota-{qid.hex[:6]}"),
str(body.get("description") or ""),
)
r = await conn.fetchrow(
"SELECT * FROM ov_quotas WHERE id=$1 AND datacenter_id=$2::uuid", qid, dc_id
)
return respond(
request, element="quota", data=_quota_entity(r, dc_id), status_code=201
)
if method == "GET" and len(parts) == 4:
r = await conn.fetchrow(
"SELECT * FROM ov_quotas WHERE id=$1::uuid AND datacenter_id=$2::uuid",
@@ -1067,16 +1186,32 @@ async def _handle_datacenters(
)
if r is None:
raise OVirtError("NotFound", "quota not found", status_code=404)
return respond(
request,
element="quota",
data={
"id": str(r["id"]),
"href": f"/ovirt-engine/api/datacenters/{dc_id}/quotas/{r['id']}",
"name": r["name"],
"description": r["description"],
},
return respond(request, element="quota", data=_quota_entity(r, dc_id))
if method == "PUT" and len(parts) == 4:
body = unwrap_entity(payload, "quota")
await conn.execute(
"""UPDATE ov_quotas SET name=COALESCE($3,name), description=COALESCE($4,description)
WHERE id=$1::uuid AND datacenter_id=$2::uuid""",
parts[3],
dc_id,
body.get("name"),
body.get("description"),
)
r = await conn.fetchrow(
"SELECT * FROM ov_quotas WHERE id=$1::uuid AND datacenter_id=$2::uuid",
parts[3],
dc_id,
)
if r is None:
raise OVirtError("NotFound", "quota not found", status_code=404)
return respond(request, element="quota", data=_quota_entity(r, dc_id))
if method == "DELETE" and len(parts) == 4:
await conn.execute(
"DELETE FROM ov_quotas WHERE id=$1::uuid AND datacenter_id=$2::uuid",
parts[3],
dc_id,
)
return Response(status_code=200)
from app.ovirt.schema_engine import handle_subcollection
if len(parts) >= 3:
@@ -1309,8 +1444,6 @@ async def _handle_clusters(
if len(parts) == 2 and method == "DELETE":
await conn.execute("DELETE FROM ov_clusters WHERE id=$1::uuid", cluster_id)
return Response(status_code=200)
if len(parts) == 3 and method == "POST":
return await respond_action(request, conn, description=f"Cluster {parts[2]}")
if len(parts) >= 3 and parts[2] == "affinitygroups":
if method == "GET" and len(parts) == 3:
rows = await conn.fetch(
@@ -1357,6 +1490,30 @@ async def _handle_clusters(
element="affinity_group",
data=_affinity_group_entity(r, cluster_id),
)
if method == "PUT" and len(parts) == 4:
body = unwrap_entity(payload, "affinity_group")
await conn.execute(
"""UPDATE ov_affinity_groups
SET name=COALESCE($3,name), enforcing=COALESCE($4,enforcing),
positive=COALESCE($5,positive), description=COALESCE($6,description)
WHERE id=$1::uuid AND cluster_id=$2::uuid""",
parts[3],
cluster_id,
body.get("name"),
body.get("enforcing"),
body.get("positive"),
body.get("description"),
)
r = await conn.fetchrow(
"SELECT * FROM ov_affinity_groups WHERE id=$1::uuid AND cluster_id=$2::uuid",
parts[3],
cluster_id,
)
if r is None:
raise OVirtError("NotFound", "affinity group not found", status_code=404)
return respond(
request, element="affinity_group", data=_affinity_group_entity(r, cluster_id)
)
if method == "DELETE" and len(parts) == 4:
await conn.execute(
"DELETE FROM ov_affinity_groups WHERE id=$1::uuid AND cluster_id=$2::uuid",
@@ -1366,6 +1523,9 @@ async def _handle_clusters(
return Response(status_code=200)
if len(parts) >= 3 and parts[2] == "networks":
return await _cluster_networks(request, conn, method, parts, payload)
if len(parts) == 3 and method == "POST":
# Known cluster actions only — do not steal collection POSTs.
return await respond_action(request, conn, description=f"Cluster {parts[2]}")
from app.ovirt.schema_engine import handle_subcollection
if len(parts) >= 3:
@@ -1694,12 +1854,27 @@ async def _handle_templates(
tid = uuid4()
vm = body.get("vm") or {}
cluster_id = None
source_vm = None
if isinstance(vm, dict) and vm.get("id"):
cluster_id = await conn.fetchval(
"SELECT cluster_id FROM ov_vms WHERE id=$1::uuid", vm["id"]
)
source_vm = await conn.fetchrow("SELECT * FROM ov_vms WHERE id=$1::uuid", vm["id"])
if source_vm is None:
raise OVirtError("NotFound", f"VM {vm['id']} not found", status_code=404)
cluster_id = source_vm["cluster_id"]
if not cluster_id:
cref = body.get("cluster") or {}
if isinstance(cref, dict) and cref.get("id"):
cluster_id = cref["id"]
elif isinstance(cref, dict) and cref.get("name"):
cluster_id = await conn.fetchval(
"SELECT id FROM ov_clusters WHERE name=$1", cref["name"]
)
if not cluster_id:
cluster_id = await conn.fetchval("SELECT id FROM ov_clusters LIMIT 1")
memory = int(
body.get("memory")
or (source_vm["memory"] if source_vm else None)
or await option_int(conn, OPT_DEFAULT_VM_MEMORY)
)
await conn.execute(
"""INSERT INTO ov_templates(id, cluster_id, name, description, memory)
VALUES($1,$2,$3,$4,$5)""",
@@ -1707,8 +1882,13 @@ async def _handle_templates(
cluster_id,
str(body.get("name") or f"tpl-{tid.hex[:6]}"),
str(body.get("description") or ""),
int(body.get("memory") or await option_int(conn, OPT_DEFAULT_VM_MEMORY)),
memory,
)
if source_vm is not None:
await _seed_template_from_vm(conn, template_id=str(tid), vm_id=str(source_vm["id"]))
await create_job(
conn, description=f"Add Template {body.get('name') or tid}", owner_id=None
)
row = await conn.fetchrow("SELECT * FROM ov_templates WHERE id=$1", tid)
return respond(request, element="template", data=template_entity(row), status_code=201)
tid = parts[1]
@@ -2034,20 +2214,42 @@ async def _handle_jobs(
if r is None:
raise OVirtError("NotFound", "job not found", status_code=404)
return respond(request, element="job", data=job_entity(r))
if len(parts) == 3 and parts[2] == "steps" and method == "GET":
rows = await conn.fetch(
"SELECT * FROM ov_job_steps WHERE job_id=$1::uuid ORDER BY number", parts[1]
)
items = [
{
"id": str(r["id"]),
"description": r["description"],
"status": r["status"],
"type": r["type"],
}
for r in rows
]
return respond(request, element="step", collection="steps", data=items)
if len(parts) >= 3 and parts[2] == "steps":
job_id = parts[1]
if len(parts) == 3 and method == "GET":
rows = await conn.fetch(
"SELECT * FROM ov_job_steps WHERE job_id=$1::uuid ORDER BY number", job_id
)
items = [
{
"id": str(r["id"]),
"href": f"/ovirt-engine/api/jobs/{job_id}/steps/{r['id']}",
"description": r["description"],
"status": r["status"],
"type": r["type"],
}
for r in rows
]
return respond(request, element="step", collection="steps", data=items)
if len(parts) == 4 and method == "GET":
r = await conn.fetchrow(
"SELECT * FROM ov_job_steps WHERE id=$1::uuid AND job_id=$2::uuid",
parts[3],
job_id,
)
if r is None:
raise OVirtError("NotFound", "step not found", status_code=404)
return respond(
request,
element="step",
data={
"id": str(r["id"]),
"href": f"/ovirt-engine/api/jobs/{job_id}/steps/{r['id']}",
"description": r["description"],
"status": r["status"],
"type": r["type"],
},
)
raise OVirtError("NotFound", "jobs path", status_code=404)
@@ -2055,7 +2257,7 @@ async def _handle_events(
request: Request, conn: Connection, method: str, parts: list[str]
) -> Response:
if len(parts) == 1 and method == "GET":
max_r = int(request.query_params.get("max") or 100)
max_r = int(request.query_params.get("max") or 500)
rows = await conn.fetch(
"SELECT * FROM ov_events ORDER BY id DESC LIMIT $1", max_r
)
@@ -2080,20 +2282,27 @@ async def _handle_events(
element="event",
data={
"id": str(r["id"]),
"href": f"/ovirt-engine/api/events/{r['id']}",
"code": r["code"],
"severity": r["severity"],
"description": r["description"],
"time": r["time"].strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
},
)
raise OVirtError("NotFound", "events path", status_code=404)
def _row_entity(r: Any, collection: str, fields: list[str]) -> dict[str, Any]:
item = {"id": str(r["id"]), "href": f"/ovirt-engine/api/{collection}/{r['id']}"}
for f in fields:
if f in r.keys():
item[f] = r[f]
return item
def _quota_entity(r: Any, dc_id: str) -> dict[str, Any]:
return {
"id": str(r["id"]),
"href": f"/ovirt-engine/api/datacenters/{dc_id}/quotas/{r['id']}",
"name": r["name"],
"description": r["description"] or "",
"data_center": {
"id": str(dc_id),
"href": f"/ovirt-engine/api/datacenters/{dc_id}",
},
}
def _affinity_group_entity(r: Any, cluster_id: str) -> dict[str, Any]:
@@ -2104,9 +2313,176 @@ def _affinity_group_entity(r: Any, cluster_id: str) -> dict[str, Any]:
"description": r["description"] or "",
"enforcing": bool(r["enforcing"]),
"positive": bool(r["positive"]),
"cluster": {
"id": str(cluster_id),
"href": f"/ovirt-engine/api/clusters/{cluster_id}",
},
}
async def _handle_top_affinity_groups(
request: Request, conn: Connection, method: str, parts: list[str]
) -> Response:
if len(parts) == 1 and method == "GET":
rows = await conn.fetch(
"SELECT * FROM ov_affinity_groups ORDER BY name"
)
items = [_affinity_group_entity(r, str(r["cluster_id"])) for r in rows]
return respond(
request, element="affinity_group", collection="affinity_groups", data=items
)
if len(parts) == 2 and method == "GET":
r = await conn.fetchrow("SELECT * FROM ov_affinity_groups WHERE id=$1::uuid", parts[1])
if r is None:
raise OVirtError("NotFound", "affinity group not found", status_code=404)
return respond(
request, element="affinity_group", data=_affinity_group_entity(r, str(r["cluster_id"]))
)
raise OVirtError("NotFound", "affinitygroups path", status_code=404)
async def _handle_top_quotas(
request: Request, conn: Connection, method: str, parts: list[str]
) -> Response:
if len(parts) == 1 and method == "GET":
rows = await conn.fetch("SELECT * FROM ov_quotas ORDER BY name")
items = [_quota_entity(r, str(r["datacenter_id"])) for r in rows]
return respond(request, element="quota", collection="quotas", data=items)
if len(parts) == 2 and method == "GET":
r = await conn.fetchrow("SELECT * FROM ov_quotas WHERE id=$1::uuid", parts[1])
if r is None:
raise OVirtError("NotFound", "quota not found", status_code=404)
return respond(
request, element="quota", data=_quota_entity(r, str(r["datacenter_id"]))
)
raise OVirtError("NotFound", "quotas path", status_code=404)
async def _copy_vm_storage_and_nics(
conn: Connection, *, source_vm_id: str, target_vm_id: str
) -> None:
"""Clone disk attachments (new disk rows) and NICs onto a target VM."""
attachments = await conn.fetch(
"""SELECT a.*, d.name AS disk_name, d.provisioned_size, d.actual_size, d.format,
d.sparse, d.storage_domain_id, d.description AS disk_description
FROM ov_disk_attachments a
JOIN ov_disks d ON d.id=a.disk_id
WHERE a.vm_id=$1::uuid""",
source_vm_id,
)
for att in attachments:
new_disk_id = uuid4()
await conn.execute(
"""INSERT INTO ov_disks(id, name, description, provisioned_size, actual_size,
format, sparse, storage_domain_id)
VALUES($1,$2,$3,$4,$5,$6,$7,$8)""",
new_disk_id,
f"{att['disk_name']}-clone" if att["disk_name"] else f"disk-{new_disk_id.hex[:8]}",
att["disk_description"] or "",
att["provisioned_size"],
att["actual_size"],
att["format"],
att["sparse"],
att["storage_domain_id"],
)
await conn.execute(
"""INSERT INTO ov_disk_attachments(id, vm_id, disk_id, active, bootable, interface)
VALUES($1,$2::uuid,$3,$4,$5,$6)""",
uuid4(),
target_vm_id,
new_disk_id,
bool(att["active"]),
bool(att["bootable"]),
att["interface"],
)
nics = await conn.fetch("SELECT * FROM ov_nics WHERE vm_id=$1::uuid", source_vm_id)
for nic in nics:
new_nic_id = uuid4()
mac_suffix = ":".join(f"{(new_nic_id.int >> (8 * i)) & 0xFF:02x}" for i in range(3))
mac = (nic["mac_address"] or "00:1a:4a:00:00:00")[:9] + mac_suffix
await conn.execute(
"""INSERT INTO ov_nics(id, vm_id, name, interface, linked, plugged, mac_address, vnic_profile_id)
VALUES($1,$2::uuid,$3,$4,$5,$6,$7,$8)""",
new_nic_id,
target_vm_id,
nic["name"],
nic["interface"],
bool(nic["linked"]),
bool(nic["plugged"]),
mac,
nic["vnic_profile_id"],
)
async def _seed_template_from_vm(
conn: Connection, *, template_id: str, vm_id: str
) -> None:
"""Materialize template nested nics/diskattachments from a source VM."""
import json as _json
from app.ovirt.ids import stable_id
nics = await conn.fetch("SELECT * FROM ov_nics WHERE vm_id=$1::uuid ORDER BY name", vm_id)
for nic in nics:
await conn.execute(
"""INSERT INTO ov_api_objects(
id, collection, name, status, parent_collection, parent_id, data
) VALUES($1,'nics',$2,'ok','templates',$3::uuid,$4::jsonb)
ON CONFLICT (id) DO NOTHING""",
stable_id("nested", "templates", template_id, "nics", nic["name"]),
nic["name"],
template_id,
_json.dumps(
{
"name": nic["name"],
"interface": nic["interface"],
"vnic_profile": (
{"id": str(nic["vnic_profile_id"])} if nic["vnic_profile_id"] else None
),
}
),
)
attachments = await conn.fetch(
"""SELECT a.*, d.name AS disk_name, d.provisioned_size, d.format
FROM ov_disk_attachments a JOIN ov_disks d ON d.id=a.disk_id
WHERE a.vm_id=$1::uuid ORDER BY d.name""",
vm_id,
)
for att in attachments:
name = att["disk_name"] or f"disk-{att['disk_id']}"
await conn.execute(
"""INSERT INTO ov_api_objects(
id, collection, name, status, parent_collection, parent_id, data
) VALUES($1,'diskattachments',$2,'ok','templates',$3::uuid,$4::jsonb)
ON CONFLICT (id) DO NOTHING""",
stable_id("nested", "templates", template_id, "diskattachments", name),
name,
template_id,
_json.dumps(
{
"name": name,
"bootable": bool(att["bootable"]),
"interface": att["interface"],
"disk": {
"name": name,
"provisioned_size": att["provisioned_size"],
"format": att["format"],
},
}
),
)
def _row_entity(r: Any, collection: str, fields: list[str]) -> dict[str, Any]:
item = {"id": str(r["id"]), "href": f"/ovirt-engine/api/{collection}/{r['id']}"}
for f in fields:
if f in r.keys():
item[f] = r[f]
return item
def _vnic_profile_entity(r: Any) -> dict[str, Any]:
return {
"id": str(r["id"]),
+93 -39
View File
@@ -41,6 +41,7 @@ _COLLECTIONS: dict[str, tuple[str, str]] = {
"networklabels": ("network_label", "ok"),
"cpuprofiles": ("cpu_profile", "ok"),
"diskprofiles": ("disk_profile", "ok"),
"diskattachments": ("disk_attachment", "ok"),
"qoss": ("qos", "ok"),
"iscsibonds": ("iscsi_bond", "ok"),
"glustervolumes": ("gluster_volume", "ok"),
@@ -65,6 +66,7 @@ _COLLECTIONS: dict[str, tuple[str, str]] = {
"devices": ("host_device", "ok"),
"sshpublickeys": ("ssh_public_key", "ok"),
"networkfilterparameters": ("network_filter_parameter", "ok"),
"storage": ("host_storage", "ok"),
}
@@ -87,7 +89,10 @@ async def handle_generic(
if len(parts) == 1:
if method == "GET":
rows = await conn.fetch(
"SELECT * FROM ov_api_objects WHERE collection=$1 ORDER BY name", collection
"""SELECT * FROM ov_api_objects
WHERE collection=$1 AND parent_id IS NULL
ORDER BY name""",
collection,
)
items = [generic_entity(collection, element, r) for r in rows]
return respond(request, element=element, collection=collection, data=items)
@@ -111,7 +116,10 @@ async def handle_generic(
if len(parts) == 2:
oid = parts[1]
row = await conn.fetchrow(
"SELECT * FROM ov_api_objects WHERE id=$1::uuid AND collection=$2", oid, collection
"""SELECT * FROM ov_api_objects
WHERE id=$1::uuid AND collection=$2 AND parent_id IS NULL""",
oid,
collection,
)
if method == "GET":
if row is None:
@@ -134,7 +142,10 @@ async def handle_generic(
return respond(request, element=element, data=generic_entity(collection, element, row))
if method == "DELETE":
await conn.execute(
"DELETE FROM ov_api_objects WHERE id=$1::uuid AND collection=$2", oid, collection
"""DELETE FROM ov_api_objects
WHERE id=$1::uuid AND collection=$2 AND parent_id IS NULL""",
oid,
collection,
)
return Response(status_code=200)
if len(parts) == 3 and method == "POST":
@@ -181,20 +192,11 @@ async def handle_subcollection(
element, _catalog_status = _meta(sub)
default_status = await option_value(conn, OPT_DEFAULT_API_OBJECT_STATUS)
collection_key = sub
if not rest and method == "GET" and sub == "permissions":
if sub == "permissions" and method == "GET":
object_type = _PARENT_OBJECT_TYPE.get(parent_collection, parent_collection.rstrip("s"))
rows = await conn.fetch(
"""SELECT p.*, r.name AS role_name, u.name AS user_name
FROM ov_permissions p
JOIN ov_roles r ON r.id = p.role_id
LEFT JOIN ov_users u ON u.id = p.user_id
WHERE p.object_type=$1 AND p.object_id=$2::uuid
ORDER BY r.name""",
object_type,
parent_id,
)
items = [
{
def _perm_item(r: Any) -> dict[str, Any]:
return {
"id": str(r["id"]),
"href": f"/ovirt-engine/api/{parent_collection}/{parent_id}/permissions/{r['id']}",
"role": {"id": str(r["role_id"]), "name": r["role_name"]},
@@ -205,30 +207,82 @@ async def handle_subcollection(
else {}
),
}
for r in rows
]
return respond(request, element="permission", collection="permissions", data=items)
if not rest and method == "GET" and sub == "tags":
if not rest:
rows = await conn.fetch(
"""SELECT p.*, r.name AS role_name, u.name AS user_name
FROM ov_permissions p
JOIN ov_roles r ON r.id = p.role_id
LEFT JOIN ov_users u ON u.id = p.user_id
WHERE p.object_type=$1 AND p.object_id=$2::uuid
ORDER BY r.name""",
object_type,
parent_id,
)
return respond(
request,
element="permission",
collection="permissions",
data=[_perm_item(r) for r in rows],
)
if len(rest) == 1:
r = await conn.fetchrow(
"""SELECT p.*, r.name AS role_name, u.name AS user_name
FROM ov_permissions p
JOIN ov_roles r ON r.id = p.role_id
LEFT JOIN ov_users u ON u.id = p.user_id
WHERE p.id=$1::uuid AND p.object_type=$2 AND p.object_id=$3::uuid""",
rest[0],
object_type,
parent_id,
)
if r is None:
raise OVirtError("NotFound", "permission not found", status_code=404)
return respond(request, element="permission", data=_perm_item(r))
if sub == "tags" and method == "GET":
object_type = _PARENT_OBJECT_TYPE.get(parent_collection, parent_collection.rstrip("s"))
rows = await conn.fetch(
"""SELECT t.*
FROM ov_tag_assignments a
JOIN ov_tags t ON t.id = a.tag_id
WHERE a.object_type=$1 AND a.object_id=$2::uuid
ORDER BY t.name""",
object_type,
parent_id,
)
items = [
{
"id": str(r["id"]),
"href": f"/ovirt-engine/api/tags/{r['id']}",
"name": r["name"],
"description": r["description"] or "",
}
for r in rows
]
return respond(request, element="tag", collection="tags", data=items)
if not rest:
rows = await conn.fetch(
"""SELECT t.*
FROM ov_tag_assignments a
JOIN ov_tags t ON t.id = a.tag_id
WHERE a.object_type=$1 AND a.object_id=$2::uuid
ORDER BY t.name""",
object_type,
parent_id,
)
items = [
{
"id": str(r["id"]),
"href": f"/ovirt-engine/api/tags/{r['id']}",
"name": r["name"],
"description": r["description"] or "",
}
for r in rows
]
return respond(request, element="tag", collection="tags", data=items)
if len(rest) == 1:
r = await conn.fetchrow(
"""SELECT t.*
FROM ov_tag_assignments a
JOIN ov_tags t ON t.id = a.tag_id
WHERE a.object_type=$1 AND a.object_id=$2::uuid AND t.id=$3::uuid""",
object_type,
parent_id,
rest[0],
)
if r is None:
raise OVirtError("NotFound", "tag not found", status_code=404)
return respond(
request,
element="tag",
data={
"id": str(r["id"]),
"href": f"/ovirt-engine/api/tags/{r['id']}",
"name": r["name"],
"description": r["description"] or "",
},
)
if not rest:
if method == "GET":
rows = await conn.fetch(
+17 -4
View File
@@ -11,7 +11,8 @@ from app.ovirt.ids import stable_id
from app.security.auth import hash_secret
MINIMAL_PROFILE = "minimal"
DEMO_PROFILE = "demo"
# Legacy name kept for imports; sized demos live in demo_datacenter.DEMO_PROFILES.
DEMO_PROFILE = "large"
async def clear_ovirt_state(conn: Connection) -> None:
@@ -239,6 +240,8 @@ async def seed_ovirt(conn: Connection) -> dict[str, Any]:
("instancetypes", "Large"),
("macpools", "Default"),
("schedulingpolicies", "evenly_distributed"),
("schedulingpolicies", "power_saving"),
("schedulingpolicies", "vm_evenly_distributed"),
("schedulingpolicyunits", "EvenlyDistributed"),
("clusterlevels", "4.5"),
("icons", "default"),
@@ -255,7 +258,8 @@ async def seed_ovirt(conn: Connection) -> dict[str, Any]:
):
await conn.execute(
"""INSERT INTO ov_api_objects(id, collection, name, status, data)
VALUES($1,$2,$3,'ok',$4::jsonb)""",
VALUES($1,$2,$3,'ok',$4::jsonb)
ON CONFLICT (id) DO NOTHING""",
stable_id("obj", collection, name),
collection,
name,
@@ -330,10 +334,19 @@ async def seed_ovirt(conn: Connection) -> dict[str, Any]:
async def ovirt_demo_summary(conn: Connection) -> dict[str, Any]:
from app.ovirt.demo_datacenter import CLUSTER_SIZES, DEMO_PROFILES
profile = await conn.fetchval("SELECT value FROM ov_demo_meta WHERE key='profile'")
active = profile or MINIMAL_PROFILE
size = CLUSTER_SIZES.get(active) or (
CLUSTER_SIZES["large"] if active == "demo" else None
)
return {
"profile": profile or MINIMAL_PROFILE,
"loaded": profile == DEMO_PROFILE,
"profile": active,
"loaded": active in DEMO_PROFILES,
"size": size.name if size else None,
"size_hosts": size.hosts if size else None,
"size_vms": size.vms if size else None,
"vms": await conn.fetchval("SELECT count(*) FROM ov_vms") or 0,
"hosts": await conn.fetchval("SELECT count(*) FROM ov_hosts") or 0,
"datacenters": await conn.fetchval("SELECT count(*) FROM ov_datacenters") or 0,
+14 -6
View File
@@ -1,4 +1,4 @@
"""CLI: python -m app.ovirt.seed_cli [--profile minimal|demo]."""
"""CLI: python -m app.ovirt.seed_cli [--profile minimal|small|large|big|demo]."""
from __future__ import annotations
@@ -9,7 +9,7 @@ import os
import asyncpg
from app.ovirt.demo_datacenter import seed_ovirt_demo
from app.ovirt.demo_datacenter import DEMO_PROFILES, normalize_cluster_size, seed_ovirt_demo
from app.ovirt.seed import seed_ovirt
@@ -20,10 +20,14 @@ async def _run(profile: str) -> dict:
)
conn = await asyncpg.connect(dsn)
try:
if profile == "demo":
result = await seed_ovirt_demo(conn)
else:
if profile == "minimal":
result = await seed_ovirt(conn)
elif profile in DEMO_PROFILES or profile in {"small", "large", "big"}:
result = await seed_ovirt_demo(conn, size=normalize_cluster_size(profile))
else:
raise SystemExit(
f"unknown profile {profile!r}; expected minimal|small|large|big|demo"
)
return result
finally:
await conn.close()
@@ -31,7 +35,11 @@ async def _run(profile: str) -> dict:
def main() -> None:
parser = argparse.ArgumentParser(description="Seed oVirt Engine simulator")
parser.add_argument("--profile", default=os.environ.get("SEED_PROFILE", "minimal"))
parser.add_argument(
"--profile",
default=os.environ.get("SEED_PROFILE", "minimal"),
help="minimal | small | large | big | demo (demo→large)",
)
args = parser.parse_args()
result = asyncio.run(_run(args.profile))
print(json.dumps(result, indent=2))
+92
View File
@@ -127,6 +127,19 @@ async def seed_nested_for_inventory(
data={"description": "Gluster feature"},
)
)
obj_rows.append(
_obj_row(
parent_collection="clusters",
parent_id=cluster_id,
collection="glustervolumes",
name="gv0",
data={
"volume_type": "distribute",
"replica_count": 1,
"status": "up",
},
)
)
for host_id in host_ids:
perm("host", host_id, f"host-{host_id}")
@@ -184,6 +197,24 @@ async def seed_nested_for_inventory(
data={"event_name": "before_vm_start"},
)
)
obj_rows.append(
_obj_row(
parent_collection="hosts",
parent_id=host_id,
collection="storage",
name="local-data",
data={"type": "data", "path": "/var/lib/ovirt/storage", "status": "up"},
)
)
obj_rows.append(
_obj_row(
parent_collection="hosts",
parent_id=host_id,
collection="katelloerrata",
name="RHSA-2024:0001",
data={"title": "Important: kernel security update"},
)
)
if tag_ids:
tag_rows.append(
(
@@ -196,6 +227,15 @@ async def seed_nested_for_inventory(
for net_id in network_ids:
perm("network", net_id, f"net-{net_id}")
obj_rows.append(
_obj_row(
parent_collection="networks",
parent_id=net_id,
collection="networklabels",
name="ovirtmgmt",
data={"description": "Management network label"},
)
)
for sd_id in storage_domain_ids:
perm("storage_domain", sd_id, f"sd-{sd_id}")
@@ -265,6 +305,24 @@ async def seed_nested_for_inventory(
data={"file": {"id": "rhel-9.iso"}},
)
)
obj_rows.append(
_obj_row(
parent_collection="templates",
parent_id=tpl_id,
collection="graphicsconsoles",
name="spice",
data={"protocol": "spice"},
)
)
obj_rows.append(
_obj_row(
parent_collection="templates",
parent_id=tpl_id,
collection="watchdogs",
name="i6300esb",
data={"model": "i6300esb", "action": "reset"},
)
)
for disk_id in disk_ids:
perm("disk", disk_id, f"disk-{disk_id}")
@@ -376,9 +434,43 @@ async def seed_nested_for_inventory(
name="pci_0000_00_02_0",
data={"capability": "pci"},
),
_obj_row(
parent_collection="vms",
parent_id=vm_id,
collection="mediateddevices",
name="mdev0",
data={"spec_params": {"mdev_type": "nvidia-11"}},
),
_obj_row(
parent_collection="vms",
parent_id=vm_id,
collection="affinitylabels",
name="label-a",
data={"description": "VM affinity label"},
),
_obj_row(
parent_collection="vms",
parent_id=vm_id,
collection="katelloerrata",
name="RHSA-2024:0001",
data={"title": "Important: qemu-kvm security update"},
),
]
)
for user_id in user_ids:
obj_rows.append(
_obj_row(
parent_collection="users",
parent_id=user_id,
collection="sshpublickeys",
name="lab-key",
data={
"content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILabSeedKey admin@lab",
},
)
)
# Scheduling policy children + role permits
for sp_name in ("evenly_distributed", "power_saving", "vm_evenly_distributed"):
sp_id = await conn.fetchval(