Skip to main content

Zorvia Web Console & HTTP API

The Fabric-compatible SPA (web/) and Axum API (zorvia api-serve) manage KubeVirt VMs from the browser. Lab front door is Kubernetes in-pod HTTPS on NodePort 30152.

Lab access​

UI: https://<HOST>:30152/
Sign-in: https://<HOST>:30152/sign-in
Health: https://<HOST>:30152/api/v1/health

Default lab bootstrap user (change for non-lab): admin / Admin@321. The user database (ZORVIA_AUTH_DB, sqlite) lives on a PersistentVolumeClaim (zorvia-auth-data in deploy/k8s.yaml), so accounts created after this bootstrap survive pod restarts and redeploys.

Deploy:

./deploy/remote-deploy.sh <host> sus --quick
# or (fills in ZORVIA_EXPOSE_HOST/HOST for you; applying k8s.yaml directly
# leaves the __ZORVIA_EXPOSE_HOST__ placeholder -- substitute it first)
kubectl apply -f deploy/k8s.yaml

Useful env on the API pod:

VariablePurpose
ZORVIA_EXPOSE_HOST / HOSTPublic host shown for NodePort SSH/VNC hints
ZORVIA_WEB_DIRSPA static root (default /usr/share/zorvia/web)
ZORVIA_JWT_SECRETJWT signing secret
ZORVIA_ADMIN_USER / ZORVIA_ADMIN_PASSWORDBootstrap admin
ZORVIA_AUTH_DBPath to the sqlite user database (default /data/auth.db in the deploy manifest, backed by a PVC)

SPA routes (Core)​

PathPage
/appDashboard
/app/vmsVM list
/app/vms/:nameVM details (power, network/port-forwards, cloud-init, snapshots)
/app/vms/:name/consoleSerial + VNC + SSH tabs
/app/createCreate VM wizard (Linux or Windows/Kryton)
/app/favoritesFavorites
/app/snapshotsSnapshots
/app/migrationsLive migration: start, list, cancel
/app/storageRook-Ceph: cluster status, pools, filesystems, object stores, StorageClass/VolumeSnapshotClass creation
/app/windowsKryton Windows machine inventory (when KRYTON_URL is set)
/app/access-controlUser & role management (admin-only β€” see below)

Auth is JWT (login) or API key. WebSockets pass ?token= because browsers cannot set Authorization on upgrades.

Create VM​

Wizard supports:

  • Guest OS: Linux | Windows
  • Linux cloud-init: hostname, username, password, SSH authorized keys, optional #cloud-config YAML
  • Windows: routed entirely through Kryton β€” picks a golden image from the live Kryton catalog and calls POST /api/v1/kryton/machines instead of /api/vms; no cloud-init, no KubeVirt-specific fields apply. Requires KRYTON_URL (see KRYTON_INTEGRATION.md).
  • Images: blank disks, PVC name (pvc:…), containerdisk refs (quay.io/…), or a downloaded golden image (datavolume:…, see below)
  • Advanced options (Linux, optional/collapsible): firmware (BIOS/UEFI/secure boot), CPU model + dedicated placement + isolate-emulator-thread, memory hugepages, machine type, TPM/RNG devices, HyperV/ACPI/APIC feature toggles
  • Additional disks / NICs (Linux, optional): the primary disk (from the Image field) and primary NIC (from the network mode toggle) cover the common case with zero extra steps; "Additional disks" (after the Root disk field) and "Additional NICs" (inside Advanced Options) let you add more of either β€” each extra disk picks a source type (blank / PVC / containerdisk / dataVolume), each extra NIC picks a network type (pod / bridge / multus / sriov). Sent as POST /api/vms's disks[]/interfaces[] arrays, with the primary disk/NIC synthesized as the first array entry so it isn't silently dropped
  • Expose: SSH (22), VNC (5900, Windows create path), RDP (3389) as Kubernetes NodePort Services
  • Auto-start after create (default)

Catalog (GET /api/images) returns blank sizes plus major Linux containerdisks. GET /api/images/cloud lists unique containerdisk images sourced from the 43 OS templates. POST /api/images/cloud/download genuinely imports the image β€” it applies a real CDI DataVolume + stable DataSource to the cluster (same manifests as zorvia image-bundle, see GOLDEN_IMAGES.md) and returns a datavolume:<name> reference usable directly as a Create VM disk image.

Day-2 operations​

OpHow
Start / stop / restart / deleteVM details, list, API
Serial console/app/vms/:name/console β†’ Terminal β†’ GET /ws/console/:name β†’ KubeVirt vmis/console
VNCConsole page VNC tab β†’ GET /ws/vnc/:name β†’ KubeVirt vmis/vnc
In-browser SSHConsole page SSH tab β†’ GET /ws/ssh/:name?user= β†’ proxies ssh or virtctl ssh, attached to a real pty (password auth works β€” an earlier plain-pipe version of this proxy couldn't complete OpenSSH's /dev/tty password prompt)
Expose SSH/VNC/RDPPort-forwards section or create-time flags β†’ NodePort Service labeled zorvia.io/vm=<name>
Cloud-init updatePOST /api/vms/:name/cloud-init (annotates VM; volume rewrite requires recreate)
ClonePOST /api/vms/:name/clone
SnapshotsCreate / list / delete / revert via Fabric /api/vms/:name/snapshots…
Hotplug CPU / memoryVM details β†’ Hotplug tab β†’ POST /api/vms/:name/hotplug/cpu|memory
Hotplug disk / NICVM details β†’ Hotplug tab β†’ POST/DELETE /api/vms/:name/hotplug/disk|nic[/:device_id]
Disk resizeVM details β†’ Disks tab β†’ POST /api/vms/:name/disks/:disk_name/resize (PVC-backed disks only, grow-only)
Live migration/app/migrations β†’ POST /api/vms/:name/migrate, list/cancel via /api/vms/:name/migrations, /api/migrations/:id/cancel
Rook-Ceph storage/app/storage β†’ bootstrap operator, create/delete pools/filesystems/object stores, provision StorageClass/VolumeSnapshotClass

CPU/memory hotplug requires the VM to have been created with cpu.maxSockets / memory.maxGuest headroom (Fabric create auto-defaults these to 4x sockets / 2x memory unless overridden) and the cluster's KubeVirt CR needs VMLiveUpdateFeatures in featureGates plus workloadUpdateStrategy.workloadUpdateMethods: ["LiveMigrate"] and configuration.liveUpdateConfiguration set β€” otherwise KubeVirt accepts the patch but only marks the VM RestartRequired instead of live-applying it. Disk hotplug requires KubeVirt's HotplugVolumes feature gate; disks must use the scsi bus (KubeVirt's admission webhook rejects any other bus for a hotplugged disk β€” Zorvia defaults to scsi when unset). NIC hotplug requires HotplugNICs + Multus and reports 501 UNSUPPORTED when unavailable.

Pause / resume call KubeVirt virtualmachineinstances/pause and …/unpause. The VMI must be Running. Linux create-time expose_vnc opens guest TCP 5900 the same way as Windows.

User & role management​

Application-level accounts and roles β€” distinct from the zorvia ServiceAccount's Kubernetes RBAC (see RBAC below), which governs what the API pod itself can do against the cluster, not who can sign in to it.

Three roles: admin, user, viewer. user can read/write VMs; viewer is read-only; only admin can manage other accounts. Roles are carried in the JWT (Claims.role) issued at login.

RoutePurpose
GET /api/v1/usersList all accounts (id, username, role, enabled, created, last_login)
POST /api/v1/usersCreate an account (username, password β€” min 8 chars, role)
DELETE /api/v1/users/:idDelete an account
PUT /api/v1/users/:id/roleChange an account's role
PUT /api/v1/users/:id/enabledEnable / disable an account

All five are admin-only, enforced by a dedicated route_layer middleware (require_admin_middleware in src/api/http_server.rs) wrapping just this route group β€” a 401 if the bearer token is missing/invalid, 403 if it's valid but not an admin. A disabled account is rejected at POST /v1/auth/login (403 "This account has been disabled") even with the correct password. Deleting, demoting, or disabling the last enabled admin is rejected with 409 β€” there's always at least one way back in.

Web UI: /app/access-control, admin-gated both in the sidebar (hidden entirely for non-admins) and in the route itself (a non-admin hitting the URL directly sees a plain "Admins only" message; the 403 from the API is the actual enforcement, this is just UX).

The user database is a local sqlite file (ZORVIA_AUTH_DB, see env table above), backed by a PersistentVolumeClaim so it survives pod restarts and redeploys β€” see Lab access above.

Fabric-compatible HTTP (under /api)​

MethodPathNotes
GET/POST/vmsList / create
GET/DELETE/vms/:nameGet / delete
POST/vms/:name/start|stop|restartPower
POST/DELETE/vms/:name/port-forwardsNodePort expose
POST/vms/:name/cloud-initAnnotate user-data
POST/vms/:name/cloneClone VM CR (+ blank/container disks)
GET/POST/vms/:name/snapshotsSnapshot list / create
DELETE/vms/:name/snapshots/:idDelete snapshot
POST/vms/:name/snapshots/:id/revertRevert
GET/imagesImage catalog
GET/vms/:name/metrics|logsGuest-agent / Prometheus samples + rolling history for the graph
GET/vms/:name/guest-insightOS/kernel/interfaces + readiness score
POST/datavolumes/:name/waitBlock until CDI DataVolume Succeeded
POST/vms/:name/wait-readyBlock until VMI Ready + guest IP
GET/readyzKube connectivity probe (readiness)
GET/metricsPrometheus text for the API process
POST/vms/:name/pause|resumeKubeVirt VMI pause / unpause (204)
GET/images/cloudTemplate-backed cloud images (name, distro, url, …)
POST/images/cloud/downloadImport a golden image: applies a real CDI DataVolume + DataSource, returns datavolume:<name>
GET/images/downloadsDownload job list
POST/vms/:name/hotplug/cpuSet target vCPU count (count)
POST/vms/:name/hotplug/memoryGrow guest memory by size_mb
POST/DELETE/vms/:name/hotplug/disk[/:device_id]Attach / detach a PVC or DataVolume (path, optional bus, default scsi)
POST/DELETE/vms/:name/hotplug/nic[/:device_id]Attach / detach a Multus network (bridge)
GET/vms/:name/disksList disks (name, bus, source, resizable)
POST/vms/:name/disks/:disk_name/resizeGrow a PVC-backed disk (size); rejects shrink and non-PVC disks
GET/vms/:name/interfacesList network interfaces (spec + live MAC/IP from VMI)
POST/vms/:name/migrateStart a live migration (VirtualMachineInstanceMigration)
GET/vms/:name/migrationsList migrations for a VM
GET/POST/migrations/:id, /migrations/:id/cancelGet / cancel a migration
POST/vms/:name/drift, /vms/:name/planDrift Guard / Change Planner against a live VM
POST/storage/rook/bootstrapInstall the Rook operator (pinned manifests)
GET/POST/DELETE/storage/rook/clusterCephCluster status / bootstrap / delete
GET/POST/storage/rook/pools, DELETE /storage/rook/pools/:nameCephBlockPool CRUD
GET/POST/storage/rook/filesystems, DELETE /storage/rook/filesystems/:nameCephFilesystem CRUD
GET/POST/storage/rook/objectstores, DELETE /storage/rook/objectstores/:nameCephObjectStore CRUD
POST/storage/rook/storage-classesProvision an RBD or CephFS StorageClass
POST/storage/rook/volume-snapshot-classesProvision a VolumeSnapshotClass

Native v1 routes remain under /api/v1/… (auth, health, namespaced VM power, snapshots, events, user & role management β€” see User & role management above).

WebSockets​

wss://<HOST>:30152/ws/console/<vm>?token=<jwt>
wss://<HOST>:30152/ws/vnc/<vm>?token=<jwt>
wss://<HOST>:30152/ws/ssh/<vm>?token=<jwt>&user=ubuntu

Server proxies to the apiserver using the in-cluster service account (trusts cluster CA; reads token_file). Clients should offer subprotocols plain.kubevirt.io (console) and binary.kubevirt.io (VNC). VMI must be Running.

RBAC​

ClusterRole zorvia includes:

  • kubevirt.io VMs / VMIs / VirtualMachineInstanceMigrations
  • subresources.kubevirt.io virtualmachineinstances/console, …/vnc, …/pause, …/unpause, …/addvolume, …/removevolume, …/addinterface, …/removeinterface
  • core services (NodePort expose), serviceaccounts, configmaps
  • storage.k8s.io storageclasses (write), snapshot.storage.k8s.io
  • ceph.rook.io, apiextensions.k8s.io, rbac.authorization.k8s.io, apps, scheduling.k8s.io (Rook operator bootstrap)
  • snapshots CRDs, PVCs, pods, events (as in deploy/k8s.yaml)

The Rook-bootstrap grants above are intentionally broad β€” installing an operator means creating its own RBAC, CRDs and Deployments. Skip POST /storage/rook/bootstrap and trim those rules if Rook is managed outside Zorvia.

Limitations​

  • Blank-disk VMs have no guest OS β€” console may connect with little/no serial output; use a containerdisk image for real SSH/VNC guest tests.
  • Linux create-time expose_vnc now creates a NodePort on guest 5900 (same as Windows).
  • Clone prefers a CDI DataVolume from the source PVC (clone_mode=cdi); falls back to an empty PVC if CDI is missing.
  • In-browser SSH needs ssh or virtctl on the API pod (the default deploy/Dockerfile.local image installs openssh-client) and a running guest with an IP or virtctl access.
  • Metrics include KubeVirt phase / paused / node. Guest CPU counters still require virt-launcher metrics.
  • Logs pull recent virt-launcher pod lines when the launcher pod is labeled kubevirt.io/vm=<name>.
  • CPU/memory hotplug depends on KubeVirt version + cluster configuration (see the note under Day-2 operations) β€” Zorvia's API and RBAC are cluster-agnostic, but without the matching KubeVirt CR settings the request succeeds (spec patched, limits enforced) while KubeVirt itself only marks the VM RestartRequired rather than live-applying it. Verify on your cluster before relying on it.
  • Disk resize only sees disks declared in the VM's spec at create time; a disk attached via hotplug isn't visible to resize until it's persisted into the spec (or the VM is recreated with it declared).
  • RDP has no in-browser proxy β€” unlike SSH/VNC, there's no /ws/rdp WebSocket gateway. expose_rdp (create-time) and a manual port-forward (day-2) both just open a NodePort on guest 3389; the port-forwards table and the Kryton Windows page both offer a "Download .rdp file" action that hands the host:port (and username, for Kryton) to the user's own native RDP client (mstsc, Microsoft Remote Desktop, Remmina, …) instead.