"""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 []