af26ad4141
Include the bad id in host/datacenter/cluster errors, expire stale auth in the console, and document Ingress annotations that preserve fault bodies.
2568 lines
105 KiB
Python
2568 lines
105 KiB
Python
"""Specialized stateful Engine API routes (root, vms, disks, hosts, networks, storage, jobs)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from asyncpg import Connection
|
|
from fastapi import APIRouter, Request, Response
|
|
|
|
from app.ovirt.common import no_such, require_cluster, require_datacenter, require_host
|
|
from app.ovirt.deps import get_db, require_auth
|
|
from app.ovirt.errors import OVirtError
|
|
from app.ovirt.jobs import create_job, respond_action
|
|
from app.ovirt.repr import (
|
|
cluster_entity,
|
|
datacenter_entity,
|
|
disk_attachment_entity,
|
|
disk_entity,
|
|
host_entity,
|
|
job_entity,
|
|
network_entity,
|
|
nic_entity,
|
|
snapshot_entity,
|
|
storage_domain_entity,
|
|
tag_entity,
|
|
template_entity,
|
|
user_entity,
|
|
vm_entity,
|
|
)
|
|
from app.ovirt.serialize import parse_body, respond, unwrap_entity, wants_xml
|
|
from app.ovirt.settings import (
|
|
OPT_DEFAULT_CLUSTER_CPU_TYPE,
|
|
OPT_DEFAULT_CPU_CORES,
|
|
OPT_DEFAULT_CPU_SOCKETS,
|
|
OPT_DEFAULT_CPU_THREADS,
|
|
OPT_DEFAULT_DC_STATUS,
|
|
OPT_DEFAULT_DISK_FORMAT,
|
|
OPT_DEFAULT_DISK_INTERFACE,
|
|
OPT_DEFAULT_DISK_SIZE,
|
|
OPT_DEFAULT_HOST_ADDRESS,
|
|
OPT_DEFAULT_HOST_CPU_CORES,
|
|
OPT_DEFAULT_HOST_MEMORY,
|
|
OPT_DEFAULT_HOST_STATUS,
|
|
OPT_DEFAULT_MAC_PREFIX,
|
|
OPT_DEFAULT_NIC_INTERFACE,
|
|
OPT_DEFAULT_NIC_NAME,
|
|
OPT_DEFAULT_OS_TYPE,
|
|
OPT_DEFAULT_SD_AVAILABLE,
|
|
OPT_DEFAULT_SD_STATUS,
|
|
OPT_DEFAULT_SD_TYPE,
|
|
OPT_DEFAULT_SNAPSHOT_DESCRIPTION,
|
|
OPT_DEFAULT_STORAGE_CONNECTION_TYPE,
|
|
OPT_DEFAULT_STORAGE_TYPE,
|
|
OPT_DEFAULT_TAG_NAME,
|
|
OPT_DEFAULT_VM_MEMORY,
|
|
OPT_DEFAULT_VM_STATUS,
|
|
OPT_DEFAULT_VM_TYPE,
|
|
OPT_HOST_ACTION_STATUS_MAP,
|
|
OPT_SD_ATTACH_ACTIVE,
|
|
OPT_SD_ATTACH_MAINTENANCE,
|
|
OPT_VM_ACTION_STATUS_MAP,
|
|
option_int,
|
|
option_json,
|
|
option_value,
|
|
product_info_from_db,
|
|
)
|
|
from app.ovirt.versioning import strip_api_prefix
|
|
|
|
router = APIRouter(tags=["engine"])
|
|
|
|
|
|
def _match(path: str, pattern: str) -> re.Match[str] | None:
|
|
return re.fullmatch(pattern, path)
|
|
|
|
|
|
async def _search_filter(rows: list[Any], search: str | None, name_attr: str = "name") -> list[Any]:
|
|
if not search:
|
|
return rows
|
|
# Support name=foo and name=foo*
|
|
m = re.search(r"name\s*=\s*([^\s]+)", search, re.I)
|
|
if not m:
|
|
return rows
|
|
pattern = m.group(1).strip("=")
|
|
case_sensitive = False
|
|
if pattern.startswith("="):
|
|
pattern = pattern[1:]
|
|
case_sensitive = True
|
|
wildcard = pattern.endswith("*")
|
|
needle = pattern[:-1] if wildcard else pattern
|
|
|
|
def ok(row: Any) -> bool:
|
|
value = str(row[name_attr])
|
|
if not case_sensitive:
|
|
value = value.lower()
|
|
n = needle.lower()
|
|
else:
|
|
n = needle
|
|
return value.startswith(n) if wildcard else value == n
|
|
|
|
return [r for r in rows if ok(r)]
|
|
|
|
|
|
async def api_root(request: Request, conn: Connection, series: str) -> Response:
|
|
from app.ovirt.contract_loader import ensure_loaded, get_runtime
|
|
|
|
ensure_loaded(series)
|
|
rt = get_runtime()
|
|
pack = rt.pack
|
|
# Entry-point link catalog comes from the active contract pack (routing metadata).
|
|
# Inventory counts + product_info come from Postgres.
|
|
links = (pack.entry_point_links if pack else []) or []
|
|
summary = {
|
|
"hosts": {
|
|
"active": await conn.fetchval("SELECT count(*) FROM ov_hosts WHERE status='up'") or 0,
|
|
"total": await conn.fetchval("SELECT count(*) FROM ov_hosts") or 0,
|
|
},
|
|
"storage_domains": {
|
|
"active": await conn.fetchval(
|
|
"SELECT count(*) FROM ov_storage_domains WHERE status='active'"
|
|
)
|
|
or 0,
|
|
"total": await conn.fetchval("SELECT count(*) FROM ov_storage_domains") or 0,
|
|
},
|
|
"users": {
|
|
"active": await conn.fetchval("SELECT count(*) FROM ov_users WHERE enabled") or 0,
|
|
"total": await conn.fetchval("SELECT count(*) FROM ov_users") or 0,
|
|
},
|
|
"vms": {
|
|
"active": await conn.fetchval("SELECT count(*) FROM ov_vms WHERE status='up'") or 0,
|
|
"total": await conn.fetchval("SELECT count(*) FROM ov_vms") or 0,
|
|
},
|
|
}
|
|
blank = await conn.fetchrow("SELECT id FROM ov_templates WHERE name='Blank' LIMIT 1")
|
|
entity: dict[str, Any] = {
|
|
"link": [{"rel": l["rel"], "href": l["href"]} for l in links],
|
|
"product_info": await product_info_from_db(conn),
|
|
"summary": summary,
|
|
"time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
|
}
|
|
if blank:
|
|
entity["special_objects"] = {
|
|
"blank_template": {
|
|
"id": str(blank["id"]),
|
|
"href": f"/ovirt-engine/api/templates/{blank['id']}",
|
|
}
|
|
}
|
|
return respond(request, element="api", data=entity)
|
|
|
|
|
|
async def handle_engine_request(request: Request) -> Response:
|
|
"""Shared Engine entrypoint used by contract-registered routes and fallbacks."""
|
|
|
|
auth = await require_auth(request)
|
|
db = get_db(request)
|
|
rel = strip_api_prefix(request.url.path)
|
|
if rel.startswith("/"):
|
|
rel = rel[1:]
|
|
method = request.method.upper()
|
|
if method == "HEAD":
|
|
method = "GET"
|
|
raw = await request.body()
|
|
payload = parse_body(raw, request.headers.get("content-type"))
|
|
try:
|
|
async with db.pool.acquire() as conn:
|
|
return await _dispatch(request, conn, auth.user_id, method, rel, payload)
|
|
except OVirtError:
|
|
raise
|
|
except (TypeError, AttributeError) as exc:
|
|
# Missing rows often crash in *_entity builders (row is None).
|
|
raise no_such("resource", rel or "/") from exc
|
|
except Exception as exc:
|
|
# Integrity / UUID parse failures from coverage mutations → client error.
|
|
name = type(exc).__name__
|
|
detail = str(exc).split("\n", 1)[0][:200]
|
|
if name in {
|
|
"ForeignKeyViolationError",
|
|
"UniqueViolationError",
|
|
"NotNullViolationError",
|
|
"CheckViolationError",
|
|
"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
|
|
|
|
|
|
# Catch-all fallback for paths outside the active pack (hidden from OpenAPI —
|
|
# each contract operation is registered individually via app.ovirt.registry).
|
|
@router.api_route(
|
|
"/ovirt-engine/api",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api-slash",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v3",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api-v3",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v3/",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api-v3-slash",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v4",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api-v4",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v4/",
|
|
methods=["GET", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:api-v4-slash",
|
|
)
|
|
async def engine_root(request: Request) -> Response:
|
|
return await handle_engine_request(request)
|
|
|
|
|
|
@router.api_route(
|
|
"/ovirt-engine/api/{path:path}",
|
|
methods=["GET", "POST", "PUT", "DELETE", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:dispatch",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v3/{path:path}",
|
|
methods=["GET", "POST", "PUT", "DELETE", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:dispatch-v3",
|
|
)
|
|
@router.api_route(
|
|
"/ovirt-engine/api/v4/{path:path}",
|
|
methods=["GET", "POST", "PUT", "DELETE", "HEAD"],
|
|
include_in_schema=False,
|
|
name="ovirt-fallback:dispatch-v4",
|
|
)
|
|
async def engine_dispatch(request: Request, path: str) -> Response:
|
|
del path
|
|
return await handle_engine_request(request)
|
|
|
|
|
|
async def _dispatch(
|
|
request: Request,
|
|
conn: Connection,
|
|
user_id: Any,
|
|
method: str,
|
|
path: str,
|
|
payload: dict[str, Any],
|
|
) -> Response:
|
|
# Engine URLs may use camelCase segments (e.g. imageTransfers); inventory keys are lowercase.
|
|
parts = [p.lower() for p in path.split("/") if p]
|
|
if not parts:
|
|
series = getattr(request.app.state, "ovirt_series", "4.5")
|
|
return await api_root(request, conn, series)
|
|
|
|
# --- VMs ---
|
|
if parts[0] == "vms":
|
|
return await _handle_vms(request, conn, user_id, method, parts, payload)
|
|
|
|
# --- Disks ---
|
|
if parts[0] == "disks":
|
|
return await _handle_disks(request, conn, user_id, method, parts, payload)
|
|
|
|
# --- Hosts ---
|
|
if parts[0] == "hosts":
|
|
return await _handle_hosts(request, conn, user_id, method, parts, payload)
|
|
|
|
# --- Datacenters ---
|
|
if parts[0] == "datacenters":
|
|
return await _handle_datacenters(request, conn, method, parts, payload)
|
|
|
|
# --- Clusters ---
|
|
if parts[0] == "clusters":
|
|
return await _handle_clusters(request, conn, method, parts, payload)
|
|
|
|
# --- Networks / vnicprofiles ---
|
|
if parts[0] == "networks":
|
|
return await _handle_networks(request, conn, method, parts, payload)
|
|
if parts[0] == "vnicprofiles":
|
|
return await _handle_vnicprofiles(request, conn, method, parts, payload)
|
|
|
|
# --- Storage ---
|
|
if parts[0] == "storagedomains":
|
|
return await _handle_storage_domains(request, conn, method, parts, payload)
|
|
if parts[0] == "storageconnections":
|
|
return await _handle_storage_connections(request, conn, method, parts, payload)
|
|
|
|
# --- Templates ---
|
|
if parts[0] == "templates":
|
|
return await _handle_templates(request, conn, method, parts, payload)
|
|
|
|
# --- Identity ---
|
|
if parts[0] == "users":
|
|
return await _handle_users(request, conn, method, parts, payload)
|
|
if parts[0] == "roles":
|
|
return await _handle_simple_table(
|
|
request,
|
|
conn,
|
|
method,
|
|
parts,
|
|
"ov_roles",
|
|
"roles",
|
|
"role",
|
|
["name", "description"],
|
|
payload,
|
|
)
|
|
if parts[0] == "groups":
|
|
return await _handle_groups(request, conn, method, parts, payload)
|
|
if parts[0] == "domains":
|
|
return await _handle_domains(request, conn, method, parts)
|
|
if parts[0] == "permissions":
|
|
return await _handle_permissions(request, conn, method, parts)
|
|
|
|
# --- Jobs / events / tags / bookmarks ---
|
|
if parts[0] == "jobs":
|
|
return await _handle_jobs(request, conn, method, parts, payload)
|
|
if parts[0] == "events":
|
|
return await _handle_events(request, conn, method, parts)
|
|
if parts[0] == "tags":
|
|
return await _handle_simple_table(
|
|
request,
|
|
conn,
|
|
method,
|
|
parts,
|
|
"ov_tags",
|
|
"tags",
|
|
"tag",
|
|
["name", "description"],
|
|
payload,
|
|
)
|
|
if parts[0] == "bookmarks":
|
|
return await _handle_simple_table(
|
|
request,
|
|
conn,
|
|
method,
|
|
parts,
|
|
"ov_bookmarks",
|
|
"bookmarks",
|
|
"bookmark",
|
|
["name", "value"],
|
|
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
|
|
|
|
return await handle_generic(request, conn, method, parts, payload)
|
|
|
|
|
|
async def _handle_vms(
|
|
request: Request,
|
|
conn: Connection,
|
|
user_id: Any,
|
|
method: str,
|
|
parts: list[str],
|
|
payload: dict[str, Any],
|
|
) -> Response:
|
|
search = request.query_params.get("search")
|
|
max_results = request.query_params.get("max")
|
|
if len(parts) == 1:
|
|
if method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_vms ORDER BY name")
|
|
rows = await _search_filter(rows, search)
|
|
if max_results:
|
|
rows = rows[: int(max_results)]
|
|
return respond(
|
|
request, element="vm", collection="vms", data=[vm_entity(r) for r in rows]
|
|
)
|
|
if method == "POST":
|
|
body = unwrap_entity(payload, "vm")
|
|
name = str(body.get("name") or f"vm-{uuid4().hex[:8]}")
|
|
cluster = body.get("cluster") or {}
|
|
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 = 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"
|
|
)
|
|
memory = int(body.get("memory") or await option_int(conn, OPT_DEFAULT_VM_MEMORY))
|
|
cpu = body.get("cpu") or {}
|
|
topology = cpu.get("topology") if isinstance(cpu, dict) else {}
|
|
sockets = int(
|
|
(topology or {}).get("sockets") or await option_int(conn, OPT_DEFAULT_CPU_SOCKETS)
|
|
)
|
|
cores = int(
|
|
(topology or {}).get("cores") or await option_int(conn, OPT_DEFAULT_CPU_CORES)
|
|
)
|
|
threads = int(
|
|
(topology or {}).get("threads") or await option_int(conn, OPT_DEFAULT_CPU_THREADS)
|
|
)
|
|
os_type = await option_value(conn, OPT_DEFAULT_OS_TYPE)
|
|
if isinstance(body.get("os"), dict):
|
|
os_type = str(body["os"].get("type") or os_type)
|
|
vm_status = await option_value(conn, OPT_DEFAULT_VM_STATUS)
|
|
vid = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_vms(id, cluster_id, template_id, name, description, status,
|
|
memory, cpu_sockets, cpu_cores, cpu_threads, os_type, type, data)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13::jsonb)""",
|
|
vid,
|
|
cluster_id,
|
|
template_id,
|
|
name,
|
|
str(body.get("description") or ""),
|
|
vm_status,
|
|
memory,
|
|
sockets,
|
|
cores,
|
|
threads,
|
|
os_type,
|
|
str(body.get("type") or await option_value(conn, OPT_DEFAULT_VM_TYPE)),
|
|
json.dumps({}),
|
|
)
|
|
# optional disk from body
|
|
row = await conn.fetchrow("SELECT * FROM ov_vms WHERE id=$1", vid)
|
|
await create_job(conn, description=f"Add VM {name}", owner_id=user_id)
|
|
return respond(request, element="vm", data=vm_entity(row), status_code=201)
|
|
|
|
vm_id = parts[1]
|
|
row = await conn.fetchrow("SELECT * FROM ov_vms WHERE id=$1::uuid", vm_id)
|
|
if row is None and method != "POST":
|
|
# allow action create paths to 404 consistently
|
|
pass
|
|
|
|
if len(parts) == 2:
|
|
if method == "GET":
|
|
if row is None:
|
|
raise OVirtError("NotFound", f"VM {vm_id} not found", status_code=404)
|
|
return respond(request, element="vm", data=vm_entity(row))
|
|
if method == "PUT":
|
|
if row is None:
|
|
raise OVirtError("NotFound", f"VM {vm_id} not found", status_code=404)
|
|
body = unwrap_entity(payload, "vm")
|
|
name = str(body.get("name") or row["name"])
|
|
description = str(body.get("description") if "description" in body else row["description"])
|
|
memory = int(body.get("memory") or row["memory"])
|
|
cpu = body.get("cpu") or {}
|
|
topology = cpu.get("topology") if isinstance(cpu, dict) else None
|
|
sockets = int((topology or {}).get("sockets") or row["cpu_sockets"])
|
|
cores = int((topology or {}).get("cores") or row["cpu_cores"])
|
|
threads = int((topology or {}).get("threads") or row["cpu_threads"])
|
|
await conn.execute(
|
|
"""UPDATE ov_vms SET name=$2, description=$3, memory=$4,
|
|
cpu_sockets=$5, cpu_cores=$6, cpu_threads=$7, updated_at=now()
|
|
WHERE id=$1::uuid""",
|
|
vm_id,
|
|
name,
|
|
description,
|
|
memory,
|
|
sockets,
|
|
cores,
|
|
threads,
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_vms WHERE id=$1::uuid", vm_id)
|
|
return respond(request, element="vm", data=vm_entity(row))
|
|
if method == "DELETE":
|
|
if row is None:
|
|
raise OVirtError("NotFound", f"VM {vm_id} not found", status_code=404)
|
|
await conn.execute("DELETE FROM ov_vms WHERE id=$1::uuid", vm_id)
|
|
await create_job(conn, description=f"Remove VM {row['name']}", owner_id=user_id)
|
|
return Response(status_code=200 if wants_xml(request) else 200)
|
|
|
|
# Subcollections first (must win over action names for POST …/vms/{id}/snapshots etc.)
|
|
if len(parts) >= 3:
|
|
sub = parts[2]
|
|
if sub == "diskattachments":
|
|
return await _vm_disk_attachments(request, conn, method, parts, payload)
|
|
if sub == "nics":
|
|
return await _vm_nics(request, conn, method, parts, payload)
|
|
if sub == "snapshots":
|
|
return await _vm_snapshots(request, conn, user_id, method, parts, payload)
|
|
if sub == "tags":
|
|
return await _vm_tags(request, conn, method, parts, payload)
|
|
if sub in {
|
|
"cdroms",
|
|
"permissions",
|
|
"graphicsconsoles",
|
|
"reporteddevices",
|
|
"sessions",
|
|
"applications",
|
|
"watchdogs",
|
|
"hostdevices",
|
|
"katelloerrata",
|
|
"numanodes",
|
|
"mediateddevices",
|
|
"affinitylabels",
|
|
"statistics",
|
|
}:
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
return await handle_subcollection(
|
|
request, conn, method, "vms", vm_id, sub, parts[3:], payload
|
|
)
|
|
|
|
if len(parts) == 3 and method == "POST":
|
|
action = parts[2]
|
|
if row is None:
|
|
raise OVirtError("NotFound", f"VM {vm_id} not found", status_code=404)
|
|
if action == "clone":
|
|
body = unwrap_entity(payload, "vm") if payload else {}
|
|
# 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,
|
|
memory, cpu_sockets, cpu_cores, cpu_threads, os_type, type, data)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'{}'::jsonb)""",
|
|
new_id,
|
|
row["cluster_id"],
|
|
row["template_id"],
|
|
new_name,
|
|
row["description"],
|
|
await option_value(conn, OPT_DEFAULT_VM_STATUS),
|
|
row["memory"],
|
|
row["cpu_sockets"],
|
|
row["cpu_cores"],
|
|
row["cpu_threads"],
|
|
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
|
|
)
|
|
if action in {
|
|
"preview_snapshot",
|
|
"commit_snapshot",
|
|
"undo_snapshot",
|
|
"export",
|
|
"detach",
|
|
"ticket",
|
|
"screenthumbnail",
|
|
}:
|
|
return await respond_action(
|
|
request, conn, description=f"VM {action}", owner_id=user_id
|
|
)
|
|
vm_actions = await option_json(conn, OPT_VM_ACTION_STATUS_MAP)
|
|
if action in vm_actions:
|
|
new_status = str(vm_actions[action])
|
|
host_id = row["host_id"]
|
|
if action == "start" and host_id is None:
|
|
host_id = await conn.fetchval(
|
|
"SELECT id FROM ov_hosts WHERE cluster_id=$1 AND status='up' LIMIT 1",
|
|
row["cluster_id"],
|
|
)
|
|
if action in {"stop", "shutdown", "suspend"}:
|
|
host_id = None if action != "suspend" else host_id
|
|
await conn.execute(
|
|
"UPDATE ov_vms SET status=$2, host_id=$3, updated_at=now() WHERE id=$1::uuid",
|
|
vm_id,
|
|
new_status,
|
|
host_id,
|
|
)
|
|
return await respond_action(
|
|
request, conn, description=f"VM {action} {row['name']}", owner_id=user_id
|
|
)
|
|
raise OVirtError("BadRequest", f"Unknown action {action}", status_code=400)
|
|
|
|
raise OVirtError("NotFound", f"No handler for /vms/{'/'.join(parts[1:])}", status_code=404)
|
|
|
|
|
|
async def _fetch_disk_attachment(conn: Connection, aid: str, vm_id: str) -> Any:
|
|
return await conn.fetchrow(
|
|
"""SELECT a.*, d.name AS disk_name
|
|
FROM ov_disk_attachments a JOIN ov_disks d ON d.id=a.disk_id
|
|
WHERE a.id=$1::uuid AND a.vm_id=$2::uuid""",
|
|
aid,
|
|
vm_id,
|
|
)
|
|
|
|
|
|
async def _vm_disk_attachments(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
vm_id = parts[1]
|
|
if len(parts) == 3:
|
|
if method == "GET":
|
|
rows = 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""",
|
|
vm_id,
|
|
)
|
|
items = [disk_attachment_entity(r, vm_id=vm_id) for r in rows]
|
|
return respond(
|
|
request, element="disk_attachment", collection="disk_attachments", data=items
|
|
)
|
|
if method == "POST":
|
|
body = unwrap_entity(payload, "disk_attachment")
|
|
disk = body.get("disk") or {}
|
|
disk_id = disk.get("id") if isinstance(disk, dict) else None
|
|
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)
|
|
)
|
|
disk_id = uuid4()
|
|
sd = await conn.fetchval("SELECT id FROM ov_storage_domains ORDER BY name LIMIT 1")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_disks(id, name, provisioned_size, actual_size, storage_domain_id)
|
|
VALUES($1,$2,$3,$3,$4)""",
|
|
disk_id,
|
|
str((disk or {}).get("name") or f"disk-{disk_id.hex[:8]}"),
|
|
size,
|
|
sd,
|
|
)
|
|
aid = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_disk_attachments(id, vm_id, disk_id, active, bootable, interface)
|
|
VALUES($1,$2::uuid,$3::uuid,$4,$5,$6)""",
|
|
aid,
|
|
vm_id,
|
|
disk_id,
|
|
bool(body.get("active", True)),
|
|
bool(body.get("bootable", False)),
|
|
str(
|
|
body.get("interface")
|
|
or await option_value(conn, OPT_DEFAULT_DISK_INTERFACE)
|
|
),
|
|
)
|
|
row = await _fetch_disk_attachment(conn, str(aid), vm_id)
|
|
return respond(
|
|
request,
|
|
element="disk_attachment",
|
|
data=disk_attachment_entity(row, vm_id=vm_id),
|
|
status_code=201,
|
|
)
|
|
if len(parts) == 4:
|
|
aid = parts[3]
|
|
if method == "GET":
|
|
r = await _fetch_disk_attachment(conn, aid, vm_id)
|
|
if r is None:
|
|
raise OVirtError("NotFound", "disk attachment not found", status_code=404)
|
|
return respond(
|
|
request,
|
|
element="disk_attachment",
|
|
data=disk_attachment_entity(r, vm_id=vm_id),
|
|
)
|
|
if method == "DELETE":
|
|
await conn.execute(
|
|
"DELETE FROM ov_disk_attachments WHERE id=$1::uuid AND vm_id=$2::uuid", aid, vm_id
|
|
)
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "diskattachments path", status_code=404)
|
|
|
|
|
|
async def _vm_nics(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
vm_id = parts[1]
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_nics WHERE vm_id=$1::uuid ORDER BY name", vm_id)
|
|
items = [nic_entity(r, vm_id=vm_id) for r in rows]
|
|
return respond(request, element="nic", collection="nics", data=items)
|
|
if len(parts) == 3 and method == "POST":
|
|
body = unwrap_entity(payload, "nic")
|
|
nid = uuid4()
|
|
profile = body.get("vnic_profile") or {}
|
|
profile_id = profile.get("id") if isinstance(profile, dict) else None
|
|
if not profile_id:
|
|
profile_id = await conn.fetchval("SELECT id FROM ov_vnic_profiles LIMIT 1")
|
|
mac_prefix = await option_value(conn, OPT_DEFAULT_MAC_PREFIX)
|
|
mac = mac_prefix + ":" + ":".join(f"{(nid.int >> (8 * i)) & 0xFF:02x}" for i in range(3))
|
|
await conn.execute(
|
|
"""INSERT INTO ov_nics(id, vm_id, name, interface, mac_address, vnic_profile_id)
|
|
VALUES($1,$2::uuid,$3,$4,$5,$6)""",
|
|
nid,
|
|
vm_id,
|
|
str(body.get("name") or await option_value(conn, OPT_DEFAULT_NIC_NAME)),
|
|
str(body.get("interface") or await option_value(conn, OPT_DEFAULT_NIC_INTERFACE)),
|
|
mac,
|
|
profile_id,
|
|
)
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_nics WHERE id=$1 AND vm_id=$2::uuid", nid, vm_id
|
|
)
|
|
return respond(
|
|
request, element="nic", data=nic_entity(row, vm_id=vm_id), status_code=201
|
|
)
|
|
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
|
|
)
|
|
return Response(status_code=200)
|
|
if len(parts) == 4 and method == "PUT":
|
|
body = unwrap_entity(payload, "nic")
|
|
profile = body.get("vnic_profile") or {}
|
|
profile_id = profile.get("id") if isinstance(profile, dict) else None
|
|
if profile_id:
|
|
await conn.execute(
|
|
"UPDATE ov_nics SET vnic_profile_id=$3::uuid, name=COALESCE($4,name) WHERE id=$1::uuid AND vm_id=$2::uuid",
|
|
nic_id,
|
|
vm_id,
|
|
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"
|
|
await conn.execute(
|
|
"UPDATE ov_nics SET plugged=$3 WHERE id=$1::uuid AND vm_id=$2::uuid",
|
|
nic_id,
|
|
vm_id,
|
|
plugged,
|
|
)
|
|
return await respond_action(
|
|
request, conn, description=f"NIC {parts[4]}", owner_id=None
|
|
)
|
|
raise OVirtError("NotFound", "nics path", status_code=404)
|
|
|
|
|
|
async def _vm_snapshots(
|
|
request: Request,
|
|
conn: Connection,
|
|
user_id: Any,
|
|
method: str,
|
|
parts: list[str],
|
|
payload: dict[str, Any],
|
|
) -> Response:
|
|
vm_id = parts[1]
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_snapshots WHERE vm_id=$1::uuid ORDER BY created_at", vm_id
|
|
)
|
|
items = [snapshot_entity(r, vm_id=vm_id) for r in rows]
|
|
return respond(request, element="snapshot", collection="snapshots", data=items)
|
|
if len(parts) == 3 and method == "POST":
|
|
body = unwrap_entity(payload, "snapshot")
|
|
sid = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_snapshots(id, vm_id, description, persist_memorystate)
|
|
VALUES($1,$2::uuid,$3,$4)""",
|
|
sid,
|
|
vm_id,
|
|
str(body.get("description") or await option_value(conn, OPT_DEFAULT_SNAPSHOT_DESCRIPTION)),
|
|
bool(body.get("persist_memorystate", False)),
|
|
)
|
|
await create_job(conn, description="Create snapshot", owner_id=user_id)
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_snapshots WHERE id=$1 AND vm_id=$2::uuid", sid, vm_id
|
|
)
|
|
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
|
|
)
|
|
return Response(status_code=200)
|
|
if len(parts) == 5 and method == "POST" and parts[4] == "restore":
|
|
return await respond_action(
|
|
request, conn, description="Restore snapshot", owner_id=user_id
|
|
)
|
|
raise OVirtError("NotFound", "snapshots path", status_code=404)
|
|
|
|
|
|
async def _vm_tags(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
vm_id = parts[1]
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"""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""",
|
|
vm_id,
|
|
)
|
|
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")
|
|
if not tag_id:
|
|
tag_id = uuid4()
|
|
await conn.execute(
|
|
"INSERT INTO ov_tags(id, name) VALUES($1,$2) ON CONFLICT (name) DO NOTHING",
|
|
tag_id,
|
|
str(body.get("name") or await option_value(conn, OPT_DEFAULT_TAG_NAME)),
|
|
)
|
|
existing = await conn.fetchval("SELECT id FROM ov_tags WHERE name=$1", body.get("name"))
|
|
tag_id = existing or tag_id
|
|
await conn.execute(
|
|
"""INSERT INTO ov_tag_assignments(id, tag_id, object_type, object_id)
|
|
VALUES($1,$2::uuid,'vm',$3::uuid) ON CONFLICT DO NOTHING""",
|
|
uuid4(),
|
|
tag_id,
|
|
vm_id,
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_tags WHERE id=$1::uuid", tag_id)
|
|
return respond(request, element="tag", data=tag_entity(row), status_code=201)
|
|
raise OVirtError("NotFound", "tags path", status_code=404)
|
|
|
|
|
|
# ---- Disks / Hosts / DC / Clusters / Networks / Storage / Templates / Users / Jobs ----
|
|
|
|
async def _handle_disks(
|
|
request: Request, conn: Connection, user_id: Any, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
search = request.query_params.get("search")
|
|
if len(parts) == 1:
|
|
if method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_disks ORDER BY name")
|
|
rows = await _search_filter(rows, search)
|
|
return respond(
|
|
request, element="disk", collection="disks", data=[disk_entity(r) for r in rows]
|
|
)
|
|
if method == "POST":
|
|
body = unwrap_entity(payload, "disk")
|
|
did = uuid4()
|
|
size = int(body.get("provisioned_size") or await option_int(conn, OPT_DEFAULT_DISK_SIZE))
|
|
sd = body.get("storage_domains", {}).get("storage_domain")
|
|
sd_id = None
|
|
if isinstance(sd, list) and sd:
|
|
sd_id = sd[0].get("id")
|
|
elif isinstance(sd, dict):
|
|
sd_id = sd.get("id")
|
|
if not sd_id:
|
|
sd_id = await conn.fetchval("SELECT id FROM ov_storage_domains LIMIT 1")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_disks(id, name, description, provisioned_size, actual_size,
|
|
format, sparse, storage_domain_id)
|
|
VALUES($1,$2,$3,$4,$4,$5,$6,$7)""",
|
|
did,
|
|
str(body.get("name") or f"disk-{did.hex[:8]}"),
|
|
str(body.get("description") or ""),
|
|
size,
|
|
str(body.get("format") or await option_value(conn, OPT_DEFAULT_DISK_FORMAT)),
|
|
bool(body.get("sparse", True)),
|
|
sd_id,
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_disks WHERE id=$1", did)
|
|
await create_job(conn, description="Add disk", owner_id=user_id)
|
|
return respond(request, element="disk", data=disk_entity(row), status_code=201)
|
|
disk_id = parts[1]
|
|
row = await conn.fetchrow("SELECT * FROM ov_disks WHERE id=$1::uuid", disk_id)
|
|
if len(parts) == 2:
|
|
if method == "GET":
|
|
if row is None:
|
|
raise OVirtError("NotFound", "disk not found", status_code=404)
|
|
return respond(request, element="disk", data=disk_entity(row))
|
|
if method == "PUT":
|
|
if row is None:
|
|
raise OVirtError("NotFound", "disk not found", status_code=404)
|
|
body = unwrap_entity(payload, "disk")
|
|
size = int(body.get("provisioned_size") or row["provisioned_size"])
|
|
await conn.execute(
|
|
"""UPDATE ov_disks SET name=COALESCE($2,name), description=COALESCE($3,description),
|
|
provisioned_size=$4, updated_at=now() WHERE id=$1::uuid""",
|
|
disk_id,
|
|
body.get("name"),
|
|
body.get("description"),
|
|
size,
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_disks WHERE id=$1::uuid", disk_id)
|
|
return respond(request, element="disk", data=disk_entity(row))
|
|
if method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_disks WHERE id=$1::uuid", disk_id)
|
|
return Response(status_code=200)
|
|
if len(parts) == 3 and method == "POST":
|
|
action = parts[2]
|
|
if action in {"copy", "export", "move", "sparsify", "reduce"}:
|
|
body = unwrap_entity(payload, "disk") if payload else {}
|
|
if action == "reduce" or "provisioned_size" in body or action == "move":
|
|
# expand/reduce size when provided
|
|
if "provisioned_size" in body:
|
|
await conn.execute(
|
|
"UPDATE ov_disks SET provisioned_size=$2, updated_at=now() WHERE id=$1::uuid",
|
|
disk_id,
|
|
int(body["provisioned_size"]),
|
|
)
|
|
return await respond_action(
|
|
request, conn, description=f"Disk {action}", owner_id=user_id
|
|
)
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "disks", disk_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "disks path", status_code=404)
|
|
|
|
|
|
async def _handle_hosts(
|
|
request: Request, conn: Connection, user_id: Any, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
search = request.query_params.get("search")
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_hosts ORDER BY name")
|
|
rows = await _search_filter(rows, search)
|
|
return respond(
|
|
request, element="host", collection="hosts", data=[host_entity(r) for r in rows]
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "host")
|
|
hid = uuid4()
|
|
cluster = body.get("cluster") or {}
|
|
cluster_id = cluster.get("id") if isinstance(cluster, dict) else None
|
|
if not cluster_id:
|
|
cluster_id = await conn.fetchval("SELECT id FROM ov_clusters LIMIT 1")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_hosts(id, cluster_id, name, address, status, memory, cpu_cores)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7)""",
|
|
hid,
|
|
cluster_id,
|
|
str(body.get("name") or f"host-{hid.hex[:6]}"),
|
|
str(body.get("address") or await option_value(conn, OPT_DEFAULT_HOST_ADDRESS)),
|
|
await option_value(conn, OPT_DEFAULT_HOST_STATUS),
|
|
int(body.get("memory") or await option_int(conn, OPT_DEFAULT_HOST_MEMORY)),
|
|
int(
|
|
(body.get("cpu") or {}).get("topology", {}).get("cores")
|
|
or await option_int(conn, OPT_DEFAULT_HOST_CPU_CORES)
|
|
),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_hosts WHERE id=$1", hid)
|
|
return respond(request, element="host", data=host_entity(row), status_code=201)
|
|
host_id = parts[1]
|
|
if len(parts) == 2:
|
|
if method == "GET":
|
|
row = await require_host(conn, host_id)
|
|
return respond(request, element="host", data=host_entity(row))
|
|
if method == "PUT":
|
|
await require_host(conn, host_id)
|
|
body = unwrap_entity(payload, "host")
|
|
await conn.execute(
|
|
"UPDATE ov_hosts SET name=COALESCE($2,name), address=COALESCE($3,address), updated_at=now() WHERE id=$1::uuid",
|
|
host_id,
|
|
body.get("name"),
|
|
body.get("address"),
|
|
)
|
|
row = await require_host(conn, host_id)
|
|
return respond(request, element="host", data=host_entity(row))
|
|
if method == "DELETE":
|
|
await require_host(conn, host_id)
|
|
await conn.execute("DELETE FROM ov_hosts WHERE id=$1::uuid", host_id)
|
|
return Response(status_code=200)
|
|
row = await conn.fetchrow("SELECT * FROM ov_hosts WHERE id=$1::uuid", host_id)
|
|
if row is None:
|
|
raise no_such("host", host_id)
|
|
if len(parts) == 3 and method == "POST":
|
|
action = parts[2].lower()
|
|
status_map = await option_json(conn, OPT_HOST_ACTION_STATUS_MAP)
|
|
if action in status_map:
|
|
await conn.execute(
|
|
"UPDATE ov_hosts SET status=$2, updated_at=now() WHERE id=$1::uuid",
|
|
host_id,
|
|
str(status_map[action]),
|
|
)
|
|
return await respond_action(
|
|
request, conn, description=f"Host {action}", owner_id=user_id
|
|
)
|
|
if len(parts) >= 3 and parts[2] == "vms":
|
|
if method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_vms WHERE host_id=$1::uuid ORDER BY name", host_id
|
|
)
|
|
return respond(
|
|
request, element="vm", collection="vms", data=[vm_entity(r) for r in rows]
|
|
)
|
|
if method == "GET" and len(parts) == 4:
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_vms WHERE id=$1::uuid AND host_id=$2::uuid",
|
|
parts[3],
|
|
host_id,
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "vm not found on host", status_code=404)
|
|
return respond(request, element="vm", data=vm_entity(row))
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "hosts", host_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "hosts path", status_code=404)
|
|
|
|
|
|
async def _handle_datacenters(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_datacenters ORDER BY name")
|
|
return respond(
|
|
request,
|
|
element="data_center",
|
|
collection="data_centers",
|
|
data=[datacenter_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "data_center")
|
|
did = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_datacenters(id, name, description, local, status)
|
|
VALUES($1,$2,$3,$4,$5)""",
|
|
did,
|
|
str(body.get("name") or f"dc-{did.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
bool(body.get("local", False)),
|
|
await option_value(conn, OPT_DEFAULT_DC_STATUS),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_datacenters WHERE id=$1", did)
|
|
return respond(request, element="data_center", data=datacenter_entity(row), status_code=201)
|
|
dc_id = parts[1]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await require_datacenter(conn, dc_id)
|
|
return respond(request, element="data_center", data=datacenter_entity(row))
|
|
if len(parts) == 2 and method == "PUT":
|
|
await require_datacenter(conn, dc_id)
|
|
body = unwrap_entity(payload, "data_center")
|
|
await conn.execute(
|
|
"UPDATE ov_datacenters SET name=COALESCE($2,name), description=COALESCE($3,description), updated_at=now() WHERE id=$1::uuid",
|
|
dc_id,
|
|
body.get("name"),
|
|
body.get("description"),
|
|
)
|
|
row = await require_datacenter(conn, dc_id)
|
|
return respond(request, element="data_center", data=datacenter_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await require_datacenter(conn, dc_id)
|
|
await conn.execute("DELETE FROM ov_datacenters WHERE id=$1::uuid", dc_id)
|
|
return Response(status_code=200)
|
|
if len(parts) == 3 and method == "POST" and parts[2] == "cleanfinishedtasks":
|
|
return await respond_action(request, conn, description="Clean finished tasks")
|
|
if len(parts) >= 3 and parts[2] == "storagedomains":
|
|
return await _dc_storage(request, conn, method, parts, payload)
|
|
if len(parts) >= 3 and parts[2] == "clusters":
|
|
return await _dc_clusters(request, conn, method, parts, payload)
|
|
if len(parts) >= 3 and parts[2] == "networks":
|
|
return await _dc_networks(request, conn, method, parts, payload)
|
|
if len(parts) >= 3 and parts[2] == "quotas":
|
|
if method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_quotas WHERE datacenter_id=$1::uuid ORDER BY name", dc_id
|
|
)
|
|
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",
|
|
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 == "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:
|
|
return await handle_subcollection(
|
|
request, conn, method, "datacenters", dc_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "datacenters path", status_code=404)
|
|
|
|
|
|
async def _dc_clusters(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
dc_id = parts[1]
|
|
await require_datacenter(conn, dc_id)
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_clusters WHERE datacenter_id=$1::uuid ORDER BY name", dc_id
|
|
)
|
|
return respond(
|
|
request,
|
|
element="cluster",
|
|
collection="clusters",
|
|
data=[cluster_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 3 and method == "POST":
|
|
body = unwrap_entity(payload, "cluster")
|
|
cid = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_clusters(id, datacenter_id, name, description, cpu_type)
|
|
VALUES($1,$2,$3,$4,$5)""",
|
|
cid,
|
|
dc_id,
|
|
str(body.get("name") or f"cluster-{cid.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
str(
|
|
(body.get("cpu") or {}).get("type")
|
|
or body.get("cpu_type")
|
|
or await option_value(conn, OPT_DEFAULT_CLUSTER_CPU_TYPE)
|
|
),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_clusters WHERE id=$1", cid)
|
|
return respond(request, element="cluster", data=cluster_entity(row), status_code=201)
|
|
if len(parts) == 4 and method == "GET":
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_clusters WHERE id=$1::uuid AND datacenter_id=$2::uuid",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
if row is None:
|
|
raise no_such("cluster", parts[3])
|
|
return respond(request, element="cluster", data=cluster_entity(row))
|
|
if len(parts) == 4 and method == "DELETE":
|
|
await conn.execute(
|
|
"DELETE FROM ov_clusters WHERE id=$1::uuid AND datacenter_id=$2::uuid",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "datacenter clusters path", status_code=404)
|
|
|
|
|
|
async def _dc_networks(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
dc_id = parts[1]
|
|
await require_datacenter(conn, dc_id)
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_networks WHERE datacenter_id=$1::uuid ORDER BY name", dc_id
|
|
)
|
|
return respond(
|
|
request,
|
|
element="network",
|
|
collection="networks",
|
|
data=[network_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 3 and method == "POST":
|
|
body = unwrap_entity(payload, "network")
|
|
nid = uuid4()
|
|
vlan = body.get("vlan") or {}
|
|
vlan_id = vlan.get("id") if isinstance(vlan, dict) else body.get("vlan_id")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_networks(id, datacenter_id, name, description, vlan_id, stp)
|
|
VALUES($1,$2,$3,$4,$5,$6)""",
|
|
nid,
|
|
dc_id,
|
|
str(body.get("name") or f"net-{nid.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
int(vlan_id) if vlan_id is not None else None,
|
|
bool(body.get("stp", False)),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_networks WHERE id=$1", nid)
|
|
return respond(request, element="network", data=network_entity(row), status_code=201)
|
|
if len(parts) == 4 and method == "GET":
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_networks WHERE id=$1::uuid AND datacenter_id=$2::uuid",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "network not found", status_code=404)
|
|
return respond(request, element="network", data=network_entity(row))
|
|
if len(parts) == 4 and method == "DELETE":
|
|
await conn.execute(
|
|
"DELETE FROM ov_networks WHERE id=$1::uuid AND datacenter_id=$2::uuid",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "datacenter networks path", status_code=404)
|
|
|
|
|
|
async def _dc_storage(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
dc_id = parts[1]
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"""SELECT s.*, a.status AS attach_status, a.id AS attachment_id
|
|
FROM ov_storage_domain_attachments a
|
|
JOIN ov_storage_domains s ON s.id=a.storage_domain_id
|
|
WHERE a.datacenter_id=$1::uuid""",
|
|
dc_id,
|
|
)
|
|
items = [storage_domain_entity(r) for r in rows]
|
|
return respond(
|
|
request, element="storage_domain", collection="storage_domains", data=items
|
|
)
|
|
if len(parts) == 3 and method == "POST":
|
|
body = unwrap_entity(payload, "storage_domain")
|
|
sd_id = body.get("id")
|
|
if not sd_id:
|
|
raise OVirtError("BadRequest", "storage domain id required", status_code=400)
|
|
await conn.execute(
|
|
"""INSERT INTO ov_storage_domain_attachments(id, storage_domain_id, datacenter_id, status)
|
|
VALUES($1,$2::uuid,$3::uuid,$4) ON CONFLICT DO NOTHING""",
|
|
uuid4(),
|
|
sd_id,
|
|
dc_id,
|
|
await option_value(conn, OPT_SD_ATTACH_ACTIVE),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_storage_domains WHERE id=$1::uuid", sd_id)
|
|
return respond(request, element="storage_domain", data=storage_domain_entity(row), status_code=201)
|
|
if len(parts) == 5 and method == "POST" and parts[4] in {"activate", "deactivate"}:
|
|
status = (
|
|
await option_value(conn, OPT_SD_ATTACH_ACTIVE)
|
|
if parts[4] == "activate"
|
|
else await option_value(conn, OPT_SD_ATTACH_MAINTENANCE)
|
|
)
|
|
await conn.execute(
|
|
"""UPDATE ov_storage_domain_attachments SET status=$3
|
|
WHERE storage_domain_id=$1::uuid AND datacenter_id=$2::uuid""",
|
|
parts[3],
|
|
dc_id,
|
|
status,
|
|
)
|
|
return await respond_action(request, conn, description=f"Storage domain {parts[4]}")
|
|
if len(parts) == 4 and method == "GET":
|
|
row = await conn.fetchrow(
|
|
"""SELECT s.*, a.status AS attach_status, a.id AS attachment_id
|
|
FROM ov_storage_domain_attachments a
|
|
JOIN ov_storage_domains s ON s.id=a.storage_domain_id
|
|
WHERE a.datacenter_id=$1::uuid AND a.storage_domain_id=$2::uuid""",
|
|
dc_id,
|
|
parts[3],
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "attached storage domain not found", status_code=404)
|
|
return respond(request, element="storage_domain", data=storage_domain_entity(row))
|
|
if len(parts) == 4 and method == "DELETE":
|
|
await conn.execute(
|
|
"""DELETE FROM ov_storage_domain_attachments
|
|
WHERE storage_domain_id=$1::uuid AND datacenter_id=$2::uuid""",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "attached storagedomains", status_code=404)
|
|
|
|
|
|
async def _handle_clusters(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_clusters ORDER BY name")
|
|
return respond(
|
|
request, element="cluster", collection="clusters", data=[cluster_entity(r) for r in rows]
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "cluster")
|
|
cid = uuid4()
|
|
dc = body.get("data_center") or {}
|
|
dc_id = dc.get("id") if isinstance(dc, dict) else None
|
|
if not dc_id:
|
|
dc_id = await conn.fetchval("SELECT id FROM ov_datacenters LIMIT 1")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_clusters(id, datacenter_id, name, description, cpu_type)
|
|
VALUES($1,$2,$3,$4,$5)""",
|
|
cid,
|
|
dc_id,
|
|
str(body.get("name") or f"cluster-{cid.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
str(
|
|
(body.get("cpu") or {}).get("type")
|
|
or await option_value(conn, OPT_DEFAULT_CLUSTER_CPU_TYPE)
|
|
),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_clusters WHERE id=$1", cid)
|
|
return respond(request, element="cluster", data=cluster_entity(row), status_code=201)
|
|
cluster_id = parts[1]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await require_cluster(conn, cluster_id)
|
|
return respond(request, element="cluster", data=cluster_entity(row))
|
|
if len(parts) == 2 and method == "PUT":
|
|
await require_cluster(conn, cluster_id)
|
|
body = unwrap_entity(payload, "cluster")
|
|
await conn.execute(
|
|
"UPDATE ov_clusters SET name=COALESCE($2,name), description=COALESCE($3,description) WHERE id=$1::uuid",
|
|
cluster_id,
|
|
body.get("name"),
|
|
body.get("description"),
|
|
)
|
|
row = await require_cluster(conn, cluster_id)
|
|
return respond(request, element="cluster", data=cluster_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await require_cluster(conn, cluster_id)
|
|
await conn.execute("DELETE FROM ov_clusters WHERE id=$1::uuid", cluster_id)
|
|
return Response(status_code=200)
|
|
if len(parts) >= 3 and parts[2] == "affinitygroups":
|
|
if method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_affinity_groups WHERE cluster_id=$1::uuid ORDER BY name",
|
|
cluster_id,
|
|
)
|
|
items = [_affinity_group_entity(r, cluster_id) for r in rows]
|
|
return respond(
|
|
request, element="affinity_group", collection="affinity_groups", data=items
|
|
)
|
|
if method == "POST" and len(parts) == 3:
|
|
body = unwrap_entity(payload, "affinity_group")
|
|
aid = uuid4()
|
|
await conn.execute(
|
|
"""INSERT INTO ov_affinity_groups(id, cluster_id, name, enforcing, positive)
|
|
VALUES($1,$2::uuid,$3,$4,$5)""",
|
|
aid,
|
|
cluster_id,
|
|
str(body.get("name") or f"ag-{aid.hex[:6]}"),
|
|
bool(body.get("enforcing", True)),
|
|
bool(body.get("positive", True)),
|
|
)
|
|
r = await conn.fetchrow(
|
|
"SELECT * FROM ov_affinity_groups WHERE id=$1 AND cluster_id=$2::uuid",
|
|
aid,
|
|
cluster_id,
|
|
)
|
|
return respond(
|
|
request,
|
|
element="affinity_group",
|
|
data=_affinity_group_entity(r, cluster_id),
|
|
status_code=201,
|
|
)
|
|
if method == "GET" and len(parts) == 4:
|
|
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 == "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",
|
|
parts[3],
|
|
cluster_id,
|
|
)
|
|
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:
|
|
return await handle_subcollection(
|
|
request, conn, method, "clusters", cluster_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "clusters path", status_code=404)
|
|
|
|
|
|
async def _cluster_networks(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
"""Cluster networks = networks of the cluster's datacenter (Engine model)."""
|
|
|
|
del payload
|
|
cluster_id = parts[1]
|
|
cluster = await require_cluster(conn, cluster_id)
|
|
dc_id = cluster["datacenter_id"]
|
|
if len(parts) == 3 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_networks WHERE datacenter_id=$1::uuid ORDER BY name", dc_id
|
|
)
|
|
return respond(
|
|
request,
|
|
element="network",
|
|
collection="networks",
|
|
data=[network_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 4 and method == "GET":
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_networks WHERE id=$1::uuid AND datacenter_id=$2::uuid",
|
|
parts[3],
|
|
dc_id,
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "network not found", status_code=404)
|
|
return respond(request, element="network", data=network_entity(row))
|
|
raise OVirtError("NotFound", "cluster networks path", status_code=404)
|
|
|
|
|
|
async def _handle_networks(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_networks ORDER BY name")
|
|
return respond(
|
|
request, element="network", collection="networks", data=[network_entity(r) for r in rows]
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "network")
|
|
nid = uuid4()
|
|
dc = body.get("data_center") or {}
|
|
dc_id = dc.get("id") if isinstance(dc, dict) else None
|
|
if not dc_id:
|
|
dc_id = await conn.fetchval("SELECT id FROM ov_datacenters LIMIT 1")
|
|
await conn.execute(
|
|
"""INSERT INTO ov_networks(id, datacenter_id, name, description, vlan_id, stp)
|
|
VALUES($1,$2,$3,$4,$5,$6)""",
|
|
nid,
|
|
dc_id,
|
|
str(body.get("name") or f"net-{nid.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
(body.get("vlan") or {}).get("id") if isinstance(body.get("vlan"), dict) else None,
|
|
bool(body.get("stp", False)),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_networks WHERE id=$1", nid)
|
|
return respond(request, element="network", data=network_entity(row), status_code=201)
|
|
net_id = parts[1]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow("SELECT * FROM ov_networks WHERE id=$1::uuid", net_id)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "network not found", status_code=404)
|
|
return respond(request, element="network", data=network_entity(row))
|
|
if len(parts) == 2 and method == "PUT":
|
|
body = unwrap_entity(payload, "network")
|
|
await conn.execute(
|
|
"UPDATE ov_networks SET name=COALESCE($2,name), description=COALESCE($3,description) WHERE id=$1::uuid",
|
|
net_id,
|
|
body.get("name"),
|
|
body.get("description"),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_networks WHERE id=$1::uuid", net_id)
|
|
return respond(request, element="network", data=network_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_networks WHERE id=$1::uuid", net_id)
|
|
return Response(status_code=200)
|
|
if len(parts) >= 3 and parts[2] == "vnicprofiles":
|
|
if method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_vnic_profiles WHERE network_id=$1::uuid ORDER BY name", net_id
|
|
)
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/vnicprofiles/{r['id']}",
|
|
"name": r["name"],
|
|
"network": {"id": net_id, "href": f"/ovirt-engine/api/networks/{net_id}"},
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(
|
|
request, element="vnic_profile", collection="vnic_profiles", data=items
|
|
)
|
|
if method == "GET" and len(parts) == 4:
|
|
r = await conn.fetchrow(
|
|
"SELECT * FROM ov_vnic_profiles WHERE id=$1::uuid AND network_id=$2::uuid",
|
|
parts[3],
|
|
net_id,
|
|
)
|
|
if r is None:
|
|
raise OVirtError("NotFound", "vnic profile not found", status_code=404)
|
|
return respond(
|
|
request,
|
|
element="vnic_profile",
|
|
data={
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/vnicprofiles/{r['id']}",
|
|
"name": r["name"],
|
|
"network": {"id": net_id, "href": f"/ovirt-engine/api/networks/{net_id}"},
|
|
},
|
|
)
|
|
if method == "DELETE" and len(parts) == 4:
|
|
await conn.execute(
|
|
"DELETE FROM ov_vnic_profiles WHERE id=$1::uuid AND network_id=$2::uuid",
|
|
parts[3],
|
|
net_id,
|
|
)
|
|
return Response(status_code=200)
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "networks", net_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "networks path", status_code=404)
|
|
|
|
|
|
async def _handle_vnicprofiles(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_vnic_profiles ORDER BY name")
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/vnicprofiles/{r['id']}",
|
|
"name": r["name"],
|
|
"network": {
|
|
"id": str(r["network_id"]),
|
|
"href": f"/ovirt-engine/api/networks/{r['network_id']}",
|
|
},
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(request, element="vnic_profile", collection="vnic_profiles", data=items)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "vnic_profile")
|
|
pid = uuid4()
|
|
net = body.get("network") or {}
|
|
net_id = net.get("id") if isinstance(net, dict) else None
|
|
if not net_id:
|
|
net_id = await conn.fetchval("SELECT id FROM ov_networks LIMIT 1")
|
|
await conn.execute(
|
|
"INSERT INTO ov_vnic_profiles(id, network_id, name) VALUES($1,$2,$3)",
|
|
pid,
|
|
net_id,
|
|
str(body.get("name") or f"profile-{pid.hex[:6]}"),
|
|
)
|
|
r = await conn.fetchrow("SELECT * FROM ov_vnic_profiles WHERE id=$1", pid)
|
|
return respond(
|
|
request,
|
|
element="vnic_profile",
|
|
data=_vnic_profile_entity(r),
|
|
status_code=201,
|
|
)
|
|
if len(parts) == 2 and method == "GET":
|
|
r = await conn.fetchrow("SELECT * FROM ov_vnic_profiles WHERE id=$1::uuid", parts[1])
|
|
if r is None:
|
|
raise OVirtError("NotFound", "vnic profile not found", status_code=404)
|
|
return respond(request, element="vnic_profile", data=_vnic_profile_entity(r))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_vnic_profiles WHERE id=$1::uuid", parts[1])
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "vnicprofiles path", status_code=404)
|
|
|
|
|
|
async def _handle_storage_domains(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_storage_domains ORDER BY name")
|
|
return respond(
|
|
request,
|
|
element="storage_domain",
|
|
collection="storage_domains",
|
|
data=[storage_domain_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "storage_domain")
|
|
sid = uuid4()
|
|
storage = body.get("storage") or {}
|
|
await conn.execute(
|
|
"""INSERT INTO ov_storage_domains(id, name, type, storage_type, status, available, used)
|
|
VALUES($1,$2,$3,$4,$5,$6,0)""",
|
|
sid,
|
|
str(body.get("name") or f"sd-{sid.hex[:6]}"),
|
|
str(body.get("type") or await option_value(conn, OPT_DEFAULT_SD_TYPE)),
|
|
str(storage.get("type") or await option_value(conn, OPT_DEFAULT_STORAGE_TYPE)),
|
|
await option_value(conn, OPT_DEFAULT_SD_STATUS),
|
|
int(body.get("available") or await option_int(conn, OPT_DEFAULT_SD_AVAILABLE)),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_storage_domains WHERE id=$1", sid)
|
|
return respond(
|
|
request, element="storage_domain", data=storage_domain_entity(row), status_code=201
|
|
)
|
|
sd_id = parts[1]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow("SELECT * FROM ov_storage_domains WHERE id=$1::uuid", sd_id)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "storage domain not found", status_code=404)
|
|
return respond(request, element="storage_domain", data=storage_domain_entity(row))
|
|
if len(parts) == 2 and method == "PUT":
|
|
body = unwrap_entity(payload, "storage_domain")
|
|
await conn.execute(
|
|
"UPDATE ov_storage_domains SET name=COALESCE($2,name) WHERE id=$1::uuid",
|
|
sd_id,
|
|
body.get("name"),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_storage_domains WHERE id=$1::uuid", sd_id)
|
|
return respond(request, element="storage_domain", data=storage_domain_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_storage_domains WHERE id=$1::uuid", sd_id)
|
|
return Response(status_code=200)
|
|
if len(parts) == 3 and method == "POST":
|
|
return await respond_action(request, conn, description=f"Storage domain {parts[2]}")
|
|
if len(parts) >= 3 and parts[2] == "disks":
|
|
if method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_disks WHERE storage_domain_id=$1::uuid ORDER BY name", sd_id
|
|
)
|
|
return respond(
|
|
request, element="disk", collection="disks", data=[disk_entity(r) for r in rows]
|
|
)
|
|
if method == "GET" and len(parts) == 4:
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM ov_disks WHERE id=$1::uuid AND storage_domain_id=$2::uuid",
|
|
parts[3],
|
|
sd_id,
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "disk not found on storage domain", status_code=404)
|
|
return respond(request, element="disk", data=disk_entity(row))
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "storagedomains", sd_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "storagedomains path", status_code=404)
|
|
|
|
|
|
async def _handle_storage_connections(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_storage_connections ORDER BY address")
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/storageconnections/{r['id']}",
|
|
"type": r["type"],
|
|
"address": r["address"],
|
|
"path": r["path"],
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(
|
|
request, element="storage_connection", collection="storage_connections", data=items
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "storage_connection")
|
|
cid = uuid4()
|
|
await conn.execute(
|
|
"INSERT INTO ov_storage_connections(id, type, address, path) VALUES($1,$2,$3,$4)",
|
|
cid,
|
|
str(
|
|
body.get("type")
|
|
or await option_value(conn, OPT_DEFAULT_STORAGE_CONNECTION_TYPE)
|
|
),
|
|
str(body.get("address") or ""),
|
|
str(body.get("path") or ""),
|
|
)
|
|
r = await conn.fetchrow("SELECT * FROM ov_storage_connections WHERE id=$1", cid)
|
|
return respond(
|
|
request,
|
|
element="storage_connection",
|
|
data=_storage_connection_entity(r),
|
|
status_code=201,
|
|
)
|
|
if len(parts) == 2 and method == "GET":
|
|
r = await conn.fetchrow("SELECT * FROM ov_storage_connections WHERE id=$1::uuid", parts[1])
|
|
if r is None:
|
|
raise OVirtError("NotFound", "storage connection not found", status_code=404)
|
|
return respond(request, element="storage_connection", data=_storage_connection_entity(r))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_storage_connections WHERE id=$1::uuid", parts[1])
|
|
return Response(status_code=200)
|
|
raise OVirtError("NotFound", "storageconnections path", status_code=404)
|
|
|
|
|
|
async def _handle_templates(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_templates ORDER BY name")
|
|
return respond(
|
|
request,
|
|
element="template",
|
|
collection="templates",
|
|
data=[template_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "template")
|
|
tid = uuid4()
|
|
vm = body.get("vm") or {}
|
|
cluster_id = None
|
|
source_vm = None
|
|
if isinstance(vm, dict) and vm.get("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)""",
|
|
tid,
|
|
cluster_id,
|
|
str(body.get("name") or f"tpl-{tid.hex[:6]}"),
|
|
str(body.get("description") or ""),
|
|
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]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow("SELECT * FROM ov_templates WHERE id=$1::uuid", tid)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "template not found", status_code=404)
|
|
return respond(request, element="template", data=template_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_templates WHERE id=$1::uuid", tid)
|
|
return Response(status_code=200)
|
|
if len(parts) == 3 and method == "POST":
|
|
return await respond_action(request, conn, description=f"Template {parts[2]}")
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "templates", tid, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "templates path", status_code=404)
|
|
|
|
|
|
async def _handle_users(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch(
|
|
"""SELECT u.*, d.name AS domain_name FROM ov_users u
|
|
JOIN ov_domains d ON d.id=u.domain_id ORDER BY u.name"""
|
|
)
|
|
return respond(
|
|
request, element="user", collection="users", data=[user_entity(r) for r in rows]
|
|
)
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow(
|
|
"""SELECT u.*, d.name AS domain_name FROM ov_users u
|
|
JOIN ov_domains d ON d.id=u.domain_id WHERE u.id=$1::uuid""",
|
|
parts[1],
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "user not found", status_code=404)
|
|
return respond(request, element="user", data=user_entity(row))
|
|
if len(parts) >= 3 and parts[2] == "permissions" and method == "GET":
|
|
user_id = parts[1]
|
|
if len(parts) == 3:
|
|
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.user_id=$1::uuid
|
|
ORDER BY r.name""",
|
|
user_id,
|
|
)
|
|
items = [_permission_entity(r) for r in rows]
|
|
return respond(request, element="permission", collection="permissions", data=items)
|
|
if len(parts) == 4:
|
|
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.user_id=$2::uuid""",
|
|
parts[3],
|
|
user_id,
|
|
)
|
|
if r is None:
|
|
raise OVirtError("NotFound", "permission not found", status_code=404)
|
|
return respond(request, element="permission", data=_permission_entity(r))
|
|
if len(parts) >= 3 and parts[2] == "roles" and method == "GET" and len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"""SELECT DISTINCT r.*
|
|
FROM ov_permissions p
|
|
JOIN ov_roles r ON r.id = p.role_id
|
|
WHERE p.user_id=$1::uuid
|
|
ORDER BY r.name""",
|
|
parts[1],
|
|
)
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/roles/{r['id']}",
|
|
"name": r["name"],
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(request, element="role", collection="roles", data=items)
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
if len(parts) >= 3:
|
|
return await handle_subcollection(
|
|
request, conn, method, "users", parts[1], parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "users path", status_code=404)
|
|
|
|
|
|
def _permission_entity(r: Any) -> dict[str, Any]:
|
|
item: dict[str, Any] = {
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/permissions/{r['id']}",
|
|
"role": {"id": str(r["role_id"]), "name": r["role_name"]},
|
|
"object": {"type": r["object_type"]},
|
|
}
|
|
if r["user_id"] is not None:
|
|
item["user"] = {"id": str(r["user_id"]), "name": r["user_name"]}
|
|
return item
|
|
|
|
|
|
def _group_entity(r: Any) -> dict[str, Any]:
|
|
gid = str(r["id"])
|
|
return {
|
|
"id": gid,
|
|
"href": f"/ovirt-engine/api/groups/{gid}",
|
|
"name": r["name"],
|
|
"domain": {"id": str(r["domain_id"]), "href": f"/ovirt-engine/api/domains/{r['domain_id']}"},
|
|
"link": [
|
|
{"rel": "permissions", "href": f"/ovirt-engine/api/groups/{gid}/permissions"},
|
|
{"rel": "tags", "href": f"/ovirt-engine/api/groups/{gid}/tags"},
|
|
],
|
|
}
|
|
|
|
|
|
async def _handle_groups(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_groups ORDER BY name")
|
|
return respond(
|
|
request,
|
|
element="group",
|
|
collection="groups",
|
|
data=[_group_entity(r) for r in rows],
|
|
)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, "group")
|
|
gid = uuid4()
|
|
domain = body.get("domain") or {}
|
|
domain_id = domain.get("id") if isinstance(domain, dict) else None
|
|
if not domain_id:
|
|
domain_id = await conn.fetchval("SELECT id FROM ov_domains ORDER BY name LIMIT 1")
|
|
if not domain_id:
|
|
raise OVirtError("BadRequest", "domain is required", status_code=400)
|
|
await conn.execute(
|
|
"INSERT INTO ov_groups(id, domain_id, name) VALUES($1,$2::uuid,$3)",
|
|
gid,
|
|
domain_id,
|
|
str(body.get("name") or f"group-{gid.hex[:6]}"),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_groups WHERE id=$1", gid)
|
|
return respond(request, element="group", data=_group_entity(row), status_code=201)
|
|
group_id = parts[1]
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow("SELECT * FROM ov_groups WHERE id=$1::uuid", group_id)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "group not found", status_code=404)
|
|
return respond(request, element="group", data=_group_entity(row))
|
|
if len(parts) == 2 and method == "PUT":
|
|
body = unwrap_entity(payload, "group")
|
|
await conn.execute(
|
|
"UPDATE ov_groups SET name=COALESCE($2,name) WHERE id=$1::uuid",
|
|
group_id,
|
|
body.get("name"),
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM ov_groups WHERE id=$1::uuid", group_id)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "group not found", status_code=404)
|
|
return respond(request, element="group", data=_group_entity(row))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute("DELETE FROM ov_groups WHERE id=$1::uuid", group_id)
|
|
return Response(status_code=200)
|
|
if len(parts) >= 3:
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
return await handle_subcollection(
|
|
request, conn, method, "groups", group_id, parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", "groups path", status_code=404)
|
|
|
|
|
|
async def _handle_domains(
|
|
request: Request, conn: Connection, method: str, parts: list[str]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_domains ORDER BY name")
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/domains/{r['id']}",
|
|
"name": r["name"],
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(request, element="domain", collection="domains", data=items)
|
|
if len(parts) == 2 and method == "GET":
|
|
row = await conn.fetchrow("SELECT * FROM ov_domains WHERE id=$1::uuid", parts[1])
|
|
if row is None:
|
|
raise OVirtError("NotFound", "domain not found", status_code=404)
|
|
return respond(
|
|
request,
|
|
element="domain",
|
|
data={
|
|
"id": str(row["id"]),
|
|
"href": f"/ovirt-engine/api/domains/{row['id']}",
|
|
"name": row["name"],
|
|
},
|
|
)
|
|
if len(parts) >= 3 and method == "GET":
|
|
domain_id = parts[1]
|
|
domain = await conn.fetchrow("SELECT id FROM ov_domains WHERE id=$1::uuid", domain_id)
|
|
if domain is None:
|
|
raise OVirtError("NotFound", "domain not found", status_code=404)
|
|
if parts[2] == "users":
|
|
if len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"""SELECT u.*, d.name AS domain_name FROM ov_users u
|
|
JOIN ov_domains d ON d.id=u.domain_id
|
|
WHERE u.domain_id=$1::uuid ORDER BY u.name""",
|
|
domain_id,
|
|
)
|
|
return respond(
|
|
request, element="user", collection="users", data=[user_entity(r) for r in rows]
|
|
)
|
|
if len(parts) == 4:
|
|
row = await conn.fetchrow(
|
|
"""SELECT u.*, d.name AS domain_name FROM ov_users u
|
|
JOIN ov_domains d ON d.id=u.domain_id
|
|
WHERE u.id=$1::uuid AND u.domain_id=$2::uuid""",
|
|
parts[3],
|
|
domain_id,
|
|
)
|
|
if row is None:
|
|
raise OVirtError("NotFound", "user not found", status_code=404)
|
|
return respond(request, element="user", data=user_entity(row))
|
|
if parts[2] == "groups":
|
|
if len(parts) == 3:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_groups WHERE domain_id=$1::uuid ORDER BY name", domain_id
|
|
)
|
|
items = [
|
|
{
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/groups/{r['id']}",
|
|
"name": r["name"],
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(request, element="group", collection="groups", data=items)
|
|
if len(parts) == 4:
|
|
r = await conn.fetchrow(
|
|
"SELECT * FROM ov_groups WHERE id=$1::uuid AND domain_id=$2::uuid",
|
|
parts[3],
|
|
domain_id,
|
|
)
|
|
if r is None:
|
|
raise OVirtError("NotFound", "group not found", status_code=404)
|
|
return respond(
|
|
request,
|
|
element="group",
|
|
data={
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/groups/{r['id']}",
|
|
"name": r["name"],
|
|
},
|
|
)
|
|
raise OVirtError("NotFound", "domains path", status_code=404)
|
|
|
|
|
|
async def _handle_permissions(
|
|
request: Request, conn: Connection, method: str, parts: list[str]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
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
|
|
ORDER BY r.name, u.name NULLS LAST"""
|
|
)
|
|
items = []
|
|
for r in rows:
|
|
item: dict[str, Any] = {
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/permissions/{r['id']}",
|
|
"role": {"id": str(r["role_id"]), "name": r["role_name"]},
|
|
"object": {"type": r["object_type"]},
|
|
}
|
|
if r["user_id"] is not None:
|
|
item["user"] = {"id": str(r["user_id"]), "name": r["user_name"]}
|
|
items.append(item)
|
|
return respond(request, element="permission", collection="permissions", data=items)
|
|
if len(parts) == 2 and method == "GET":
|
|
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""",
|
|
parts[1],
|
|
)
|
|
if r is None:
|
|
raise OVirtError("NotFound", "permission not found", status_code=404)
|
|
data: dict[str, Any] = {
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/permissions/{r['id']}",
|
|
"role": {"id": str(r["role_id"]), "name": r["role_name"]},
|
|
"object": {"type": r["object_type"]},
|
|
}
|
|
if r["user_id"] is not None:
|
|
data["user"] = {"id": str(r["user_id"]), "name": r["user_name"]}
|
|
return respond(request, element="permission", data=data)
|
|
raise OVirtError("NotFound", "permissions path", status_code=404)
|
|
|
|
|
|
async def _handle_jobs(
|
|
request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any]
|
|
) -> Response:
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch("SELECT * FROM ov_jobs ORDER BY started DESC LIMIT 200")
|
|
items = [job_entity(r) for r in rows]
|
|
return respond(request, element="job", collection="jobs", data=items)
|
|
if len(parts) == 2 and method == "GET":
|
|
r = await conn.fetchrow("SELECT * FROM ov_jobs WHERE id=$1::uuid", parts[1])
|
|
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":
|
|
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)
|
|
|
|
|
|
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 500)
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM ov_events ORDER BY id DESC LIMIT $1", max_r
|
|
)
|
|
items = [
|
|
{
|
|
"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"),
|
|
}
|
|
for r in rows
|
|
]
|
|
return respond(request, element="event", collection="events", data=items)
|
|
if len(parts) == 2 and method == "GET":
|
|
r = await conn.fetchrow("SELECT * FROM ov_events WHERE id=$1::bigint", int(parts[1]))
|
|
if r is None:
|
|
raise OVirtError("NotFound", "event not found", status_code=404)
|
|
return respond(
|
|
request,
|
|
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 _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]:
|
|
return {
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/clusters/{cluster_id}/affinitygroups/{r['id']}",
|
|
"name": r["name"],
|
|
"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"]),
|
|
"href": f"/ovirt-engine/api/vnicprofiles/{r['id']}",
|
|
"name": r["name"],
|
|
"network": {
|
|
"id": str(r["network_id"]),
|
|
"href": f"/ovirt-engine/api/networks/{r['network_id']}",
|
|
},
|
|
}
|
|
|
|
|
|
def _storage_connection_entity(r: Any) -> dict[str, Any]:
|
|
return {
|
|
"id": str(r["id"]),
|
|
"href": f"/ovirt-engine/api/storageconnections/{r['id']}",
|
|
"type": r["type"],
|
|
"address": r["address"],
|
|
"path": r["path"],
|
|
}
|
|
|
|
|
|
async def _handle_simple_table(
|
|
request: Request,
|
|
conn: Connection,
|
|
method: str,
|
|
parts: list[str],
|
|
table: str,
|
|
collection: str,
|
|
element: str,
|
|
fields: list[str],
|
|
payload: dict[str, Any] | None = None,
|
|
) -> Response:
|
|
payload = payload or {}
|
|
if len(parts) == 1 and method == "GET":
|
|
rows = await conn.fetch(f"SELECT * FROM {table} ORDER BY name") # noqa: S608
|
|
items = [_row_entity(r, collection, fields) for r in rows]
|
|
return respond(request, element=element, collection=collection, data=items)
|
|
if len(parts) == 1 and method == "POST":
|
|
body = unwrap_entity(payload, element)
|
|
oid = uuid4()
|
|
cols = ["id", *fields]
|
|
values: list[Any] = [oid]
|
|
for f in fields:
|
|
values.append(body.get(f) or ("" if f != "name" else f"{element}-{oid.hex[:6]}"))
|
|
placeholders = ", ".join(f"${i}" for i in range(1, len(cols) + 1))
|
|
await conn.execute(
|
|
f"INSERT INTO {table}({', '.join(cols)}) VALUES({placeholders})",
|
|
*values,
|
|
)
|
|
r = await conn.fetchrow(f"SELECT * FROM {table} WHERE id=$1", oid) # noqa: S608
|
|
return respond(
|
|
request, element=element, data=_row_entity(r, collection, fields), status_code=201
|
|
)
|
|
if len(parts) == 2 and method == "GET":
|
|
r = await conn.fetchrow(f"SELECT * FROM {table} WHERE id=$1::uuid", parts[1]) # noqa: S608
|
|
if r is None:
|
|
raise OVirtError("NotFound", f"{element} not found", status_code=404)
|
|
return respond(request, element=element, data=_row_entity(r, collection, fields))
|
|
if len(parts) == 2 and method == "PUT":
|
|
body = unwrap_entity(payload, element)
|
|
sets = []
|
|
values: list[Any] = [parts[1]]
|
|
for f in fields:
|
|
if f in body:
|
|
values.append(body[f])
|
|
sets.append(f"{f}=${len(values)}")
|
|
if sets:
|
|
await conn.execute(
|
|
f"UPDATE {table} SET {', '.join(sets)} WHERE id=$1::uuid", # noqa: S608
|
|
*values,
|
|
)
|
|
r = await conn.fetchrow(f"SELECT * FROM {table} WHERE id=$1::uuid", parts[1]) # noqa: S608
|
|
return respond(request, element=element, data=_row_entity(r, collection, fields))
|
|
if len(parts) == 2 and method == "DELETE":
|
|
await conn.execute(f"DELETE FROM {table} WHERE id=$1::uuid", parts[1]) # noqa: S608
|
|
return Response(status_code=200)
|
|
if len(parts) >= 3:
|
|
from app.ovirt.schema_engine import handle_subcollection
|
|
|
|
return await handle_subcollection(
|
|
request, conn, method, collection, parts[1], parts[2], parts[3:], payload
|
|
)
|
|
raise OVirtError("NotFound", f"{collection} path", status_code=404)
|