546 lines
19 KiB
Python
546 lines
19 KiB
Python
"""Permissions, identity, guest extras, host detail, console tickets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, Query, Response
|
|
|
|
from app.db.pool import Database
|
|
from app.dependencies import get_database
|
|
from app.vsphere import inventory
|
|
from app.vsphere.domain import vm_ops
|
|
from app.vsphere.errors import invalid_argument, not_found
|
|
from app.vsphere.security.authz import require_admin, require_privilege, require_read
|
|
from app.vsphere.security.session import SessionInfo
|
|
|
|
router = APIRouter(tags=["vSphere Platform"])
|
|
|
|
|
|
@router.get("/api/vcenter/privilege")
|
|
async def list_privileges(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, str]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(database, "GET", "/api/vcenter/privilege")
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/authorization/roles")
|
|
async def list_roles(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(database, "GET", "/api/vcenter/authorization/roles")
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/authorization/permissions")
|
|
async def list_permissions(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
pool = database.pool # type: ignore[attr-defined]
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch(
|
|
"SELECT id, principal, role, entity_moid, propagate FROM vsphere_permissions ORDER BY id"
|
|
)
|
|
return [
|
|
{
|
|
"id": row["id"],
|
|
"principal": row["principal"],
|
|
"role": row["role"],
|
|
"entity": row["entity_moid"],
|
|
"propagate": row["propagate"],
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
@router.post("/api/vcenter/authorization/permissions")
|
|
async def create_permission(
|
|
body: dict[str, Any],
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> dict[str, Any]:
|
|
principal = str(body.get("principal") or "").strip()
|
|
role = str(body.get("role") or "").strip()
|
|
if not principal or not role:
|
|
raise invalid_argument("principal and role are required")
|
|
entity = body.get("entity") or body.get("entity_moid")
|
|
propagate = bool(body.get("propagate", True))
|
|
pool = database.pool # type: ignore[attr-defined]
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
INSERT INTO vsphere_permissions (principal, role, entity_moid, propagate)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id
|
|
""",
|
|
principal,
|
|
role,
|
|
None if entity in (None, "", "null") else str(entity),
|
|
propagate,
|
|
)
|
|
return {"id": row["id"]}
|
|
|
|
|
|
@router.delete("/api/vcenter/authorization/permissions/{permission_id}")
|
|
async def delete_permission(
|
|
permission_id: int,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> Response:
|
|
pool = database.pool # type: ignore[attr-defined]
|
|
async with pool.acquire() as conn:
|
|
result = await conn.execute(
|
|
"DELETE FROM vsphere_permissions WHERE id = $1",
|
|
permission_id,
|
|
)
|
|
if not result.endswith("1"):
|
|
raise not_found(f"Permission {permission_id} not found")
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/api/vcenter/namespaces")
|
|
async def list_namespaces(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(database, "GET", "/api/vcenter/namespaces")
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/namespace-management/nsx-tier0-gateway")
|
|
async def nsx_tier0_gateway(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
path = "/api/vcenter/namespace-management/nsx-tier0-gateway"
|
|
payload = await api_state.get_payload(database, "GET", path)
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/namespace-management/networks")
|
|
async def wcp_networks(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
path = "/api/vcenter/namespace-management/networks"
|
|
payload = await api_state.get_payload(database, "GET", path)
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/namespace-management/virtual-machine-classes")
|
|
async def vm_classes(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
path = "/api/vcenter/namespace-management/virtual-machine-classes"
|
|
payload = await api_state.get_payload(database, "GET", path)
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/identity/providers")
|
|
async def identity_providers(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import platform_surface
|
|
|
|
return await platform_surface.list_identity_providers(database)
|
|
|
|
|
|
@router.post("/api/vcenter/identity/providers")
|
|
async def create_identity_provider(
|
|
body: dict[str, Any],
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import platform_surface
|
|
|
|
return await platform_surface.upsert_identity_provider(database, body)
|
|
|
|
|
|
@router.get("/api/vcenter/identity/providers/{provider}")
|
|
async def get_identity_provider(
|
|
provider: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import platform_surface
|
|
|
|
item = await platform_surface.get_identity_provider(database, provider)
|
|
if item is not None:
|
|
return item
|
|
raise not_found(f"Identity provider {provider} not found")
|
|
|
|
|
|
@router.patch("/api/vcenter/identity/providers/{provider}")
|
|
@router.put("/api/vcenter/identity/providers/{provider}")
|
|
async def update_identity_provider(
|
|
provider: str,
|
|
body: dict[str, Any],
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import platform_surface
|
|
|
|
payload = {**body, "provider": provider}
|
|
return await platform_surface.upsert_identity_provider(database, payload)
|
|
|
|
|
|
@router.delete("/api/vcenter/identity/providers/{provider}")
|
|
async def delete_identity_provider(
|
|
provider: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> Response:
|
|
from app.vsphere.domain import platform_surface
|
|
|
|
if not await platform_surface.delete_identity_provider(database, provider):
|
|
raise not_found(f"Identity provider {provider} not found")
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/api/vcenter/certificate-management/vcenter/tls")
|
|
async def machine_tls(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(
|
|
database, "GET", "/api/vcenter/certificate-management/vcenter/tls"
|
|
)
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
@router.get("/api/vcenter/certificate-management/vcenter/tls-csr")
|
|
async def machine_tls_csr_get(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(
|
|
database, "GET", "/api/vcenter/certificate-management/vcenter/tls-csr"
|
|
)
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
@router.post("/api/vcenter/certificate-management/vcenter/tls-csr")
|
|
async def machine_tls_csr_create(
|
|
body: dict[str, Any] | None = None,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_admin),
|
|
) -> dict[str, Any]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
del body
|
|
payload = await api_state.get_payload(
|
|
database, "GET", "/api/vcenter/certificate-management/vcenter/tls-csr"
|
|
)
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
@router.get("/api/vcenter/certificate-management/vcenter/trusted-root-chains")
|
|
async def trusted_root_chains(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(
|
|
database, "GET", "/api/vcenter/certificate-management/vcenter/trusted-root-chains"
|
|
)
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/customization")
|
|
async def guest_customization_get(
|
|
vm: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
obj = await vm_ops.require_vm(database, vm)
|
|
customization = obj.props.get("customization")
|
|
if isinstance(customization, dict) and customization:
|
|
return customization
|
|
# Seed / probe may wipe the field with POST {}; keep a non-empty lab view.
|
|
return {
|
|
"name": obj.name,
|
|
"status": "PENDING",
|
|
"spec": {"hostname": obj.name, "domain": "lab.local"},
|
|
}
|
|
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/networking")
|
|
async def guest_networking(
|
|
vm: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
return await vm_ops.guest_networking(database, vm)
|
|
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/power")
|
|
async def guest_power_get(
|
|
vm: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, str]:
|
|
obj = await vm_ops.require_vm(database, vm)
|
|
state = str(obj.props.get("power_state") or "POWERED_OFF")
|
|
return {"state": "RUNNING" if state == "POWERED_ON" else "NOT_RUNNING"}
|
|
|
|
|
|
@router.post("/api/vcenter/vm/{vm}/guest/power")
|
|
async def guest_power_post(
|
|
vm: str,
|
|
action: str = Query(...),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.PowerOn")),
|
|
) -> Response:
|
|
mapping = {"reboot": "reset", "shutdown": "stop", "standby": "suspend"}
|
|
await vm_ops.set_power(database, vm, mapping.get(action, action))
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.post("/api/vcenter/vm/{vm}/tools")
|
|
async def tools_upgrade(
|
|
vm: str,
|
|
action: str = Query("upgrade"),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.DeviceConnection")),
|
|
) -> dict[str, str]:
|
|
del action
|
|
await vm_ops.require_vm(database, vm)
|
|
from app.vsphere.domain import tasks as task_store
|
|
|
|
task_id = await task_store.create_task(
|
|
database,
|
|
description=f"Upgrade tools {vm}",
|
|
service="com.vmware.vcenter.vm.tools",
|
|
operation="upgrade",
|
|
result={"vm": vm},
|
|
)
|
|
return {"task": task_id}
|
|
|
|
|
|
@router.post("/api/vcenter/vm/{vm}/console/tickets")
|
|
async def console_tickets(
|
|
vm: str,
|
|
body: dict[str, Any] | None = None,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.ConsoleInteract")),
|
|
) -> dict[str, Any]:
|
|
del body
|
|
return await vm_ops.console_ticket(database, vm)
|
|
|
|
|
|
@router.post("/api/vcenter/vm/{vm}/guest/customization")
|
|
async def guest_customization(
|
|
vm: str,
|
|
body: dict[str, Any],
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Config.Rename")),
|
|
) -> dict[str, str]:
|
|
obj = await vm_ops.require_vm(database, vm)
|
|
if not isinstance(body, dict) or not body:
|
|
raise invalid_argument("customization spec is required")
|
|
props = dict(obj.props)
|
|
props["customization"] = body
|
|
await inventory.update_props(database, vm, props)
|
|
from app.vsphere.domain import tasks as task_store
|
|
|
|
task_id = await task_store.create_task(
|
|
database,
|
|
description=f"Customize {vm}",
|
|
service="com.vmware.vcenter.vm.guest.customization",
|
|
operation="set",
|
|
result={"vm": vm},
|
|
)
|
|
return {"task": task_id}
|
|
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/local-filesystem")
|
|
async def guest_local_filesystem(
|
|
vm: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
obj = await vm_ops.require_vm(database, vm)
|
|
filesystems = obj.props.get("guest_filesystems")
|
|
if isinstance(filesystems, dict) and filesystems:
|
|
return filesystems
|
|
return {
|
|
"filesystems": {
|
|
"/": {"capacity": 42949672960, "free_space": 21474836480},
|
|
}
|
|
}
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/filesystem")
|
|
async def guest_filesystem_get(
|
|
vm: str,
|
|
path: str = Query("/"),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
if path in {"/", "/tmp", "/etc"}:
|
|
entries = await vm_ops.guest_list_files(database, vm, path)
|
|
return {"path": path, "type": "DIRECTORY", "files": entries}
|
|
content = await vm_ops.guest_read_file(database, vm, path)
|
|
return {"path": path, "type": "FILE", "content": content, "size": len(content.encode("utf-8"))}
|
|
|
|
|
|
@router.put("/api/vcenter/vm/{vm}/guest/filesystem")
|
|
async def guest_filesystem_put(
|
|
vm: str,
|
|
body: dict[str, Any],
|
|
path: str = Query(...),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.DeviceConnection")),
|
|
) -> Response:
|
|
content = str(body.get("content") or "")
|
|
await vm_ops.guest_write_file(database, vm, path, content, overwrite=True)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.delete("/api/vcenter/vm/{vm}/guest/filesystem")
|
|
async def guest_filesystem_delete(
|
|
vm: str,
|
|
path: str = Query(...),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.DeviceConnection")),
|
|
) -> Response:
|
|
await vm_ops.guest_delete_file(database, vm, path)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/api/vcenter/vm/{vm}/guest/filesystem/files")
|
|
async def guest_filesystem_list(
|
|
vm: str,
|
|
path: str = Query("/"),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
return await vm_ops.guest_list_files(database, vm, path)
|
|
|
|
|
|
@router.post("/api/vcenter/vm/{vm}")
|
|
async def vm_action(
|
|
vm: str,
|
|
action: str = Query(...),
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_privilege("VirtualMachine.Provisioning.MarkAsTemplate")),
|
|
) -> dict[str, str]:
|
|
if action == "mark-as-template":
|
|
task_id = await vm_ops.set_template(database, vm, template=True)
|
|
elif action in {"mark-as-vm", "mark-as-virtual-machine"}:
|
|
task_id = await vm_ops.set_template(database, vm, template=False)
|
|
elif action == "unregister":
|
|
task_id = await vm_ops.unregister_vm(database, vm)
|
|
else:
|
|
raise invalid_argument(f"unsupported action {action}")
|
|
return {"task": task_id}
|
|
|
|
|
|
@router.get("/api/vcenter/host/{host}/storage/storage-device")
|
|
async def host_storage(
|
|
host: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
obj = await inventory.get_object(database, host)
|
|
if obj is None or obj.type != "HostSystem":
|
|
raise not_found(f"Host {host} not found")
|
|
devices = obj.props.get("storage_devices")
|
|
return list(devices) if isinstance(devices, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/host/{host}/networking")
|
|
async def host_networking(
|
|
host: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> dict[str, Any]:
|
|
obj = await inventory.get_object(database, host)
|
|
if obj is None or obj.type != "HostSystem":
|
|
raise not_found(f"Host {host} not found")
|
|
networking = obj.props.get("networking")
|
|
return networking if isinstance(networking, dict) else {}
|
|
|
|
|
|
@router.get("/api/vcenter/folder/{folder}/children")
|
|
async def folder_children(
|
|
folder: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, str]]:
|
|
parent = await inventory.get_object(database, folder)
|
|
if parent is None:
|
|
raise not_found(f"Folder {folder} not found")
|
|
children = [obj for obj in await inventory.list_objects(database) if obj.parent_moid == folder]
|
|
return [{"moid": c.moid, "type": c.type, "name": c.name} for c in children]
|
|
|
|
|
|
@router.get("/api/vcenter/storage/policies/{policy}/vm")
|
|
async def policy_vms(
|
|
policy: str,
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[str]:
|
|
objects = await inventory.list_objects(database, type_name="VirtualMachine")
|
|
if policy in {"policy-default", "default"}:
|
|
return [obj.moid for obj in objects]
|
|
# Named non-default policies attach to every 10th VM for lab demos.
|
|
return [obj.moid for i, obj in enumerate(objects) if i % 10 == 0]
|
|
|
|
|
|
@router.get("/api/vapi/metadata/metamodel/service")
|
|
async def metamodel_services(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[str]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(database, "GET", "/api/vapi/metadata/metamodel/service")
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vapi/metadata/authentication/component")
|
|
async def authentication_components(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[str]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(
|
|
database, "GET", "/api/vapi/metadata/authentication/component"
|
|
)
|
|
return payload if isinstance(payload, list) else []
|
|
|
|
|
|
@router.get("/api/vcenter/activity-history")
|
|
async def activity_history(
|
|
database: Database = Depends(get_database),
|
|
_: SessionInfo = Depends(require_read),
|
|
) -> list[dict[str, Any]]:
|
|
from app.vsphere.domain import api_state
|
|
|
|
payload = await api_state.get_payload(database, "GET", "/api/vcenter/activity-history")
|
|
return payload if isinstance(payload, list) else []
|