Docs / API Reference

SimpleVirt API Reference

This is the HTTP API of SimpleVirt-Command, the controller. It is the same API the SimpleVirt-Command web console uses: there is no second, richer interface behind it, and nothing the console can do is unavailable here.

It is written for someone integrating a client — a provisioning system, a monitoring collector, a script — against a running controller. For what the product does and how an operator drives it, see the user guide; for how the pieces fit together, the architecture guide.

Every endpoint in this document was read from the controller's route table. Each one names the privilege it requires, because as of this release every route enforces one server-side — a read included. A client that holds a session but not the privilege gets 403, not data.


Contents


Base URL and transport

All paths are relative to the controller's HTTPS listener:

https://<simplevirt-command-address>/api/v1/...

TLS is always on. A fresh controller serves a self-signed certificate; an operator can install their own (see controller settings). A client must either trust the installed certificate or pin it — do not disable verification in production code.

Two paths sit outside /api/v1:

Method Path Privilege Description
GET /healthz none Liveness probe. Answers {"status":"ok"} with no session. Carries no fleet data.
ANY / none The embedded web console and its static assets. An unknown /api/ path under it answers 404.

Authentication

SimpleVirt-Command uses a session cookie plus a CSRF double-submit token. This is stated once here and is not repeated per endpoint, because it applies to all of them.

1. Sign in.

POST /api/v1/auth/login
Content-Type: application/json

{"username": "alice", "password": "..."}
{"username": "alice", "csrf_token": "…"}

The response also sets the session cookie:

Set-Cookie: simplevirt_command_session=…; Path=/; HttpOnly; Secure; SameSite=Strict

2. Send the cookie on every request. Any client that keeps a cookie jar does this for you.

3. Send the CSRF token on every mutating request. Every POST, PUT, PATCH and DELETE must carry the token from sign-in in a header:

X-CSRF-Token: <csrf_token>

A missing or wrong token is 403 {"error":"invalid or missing CSRF token"}. GET requests carry no CSRF token.

4. Recover the token without re-authenticating — a client that restarted but still holds a valid cookie can re-read its identity, its token and its privilege set from GET /api/v1/auth/me.

Accounts and directories. Sign-in checks the controller's local accounts first, then LDAP/Active Directory when one is configured. Local accounts are the break-glass path and are always admitted; a directory account is admitted only if it, or a group it belongs to, maps to a role. Repeated failures are rate-limited per source address and answer 429.

Sessions expire. An expired or unknown cookie is 401; sign in again. There is no refresh endpoint and no bearer-token or API-key mechanism in this release — a client integrating against SimpleVirt-Command signs in with an account like any other principal. Give it its own account with a role scoped to what it actually needs.

Request and response conventions

  • JSON in, JSON out. Send Content-Type: application/json on request bodies. The exceptions are the streaming endpoints, marked where they occur.

  • Request bodies are strict. Unknown fields are rejected with 400 {"error":"invalid JSON request body"}. A client must not send fields this version does not define — including ones it saw in a newer release.

  • Response objects are not strict. New fields are added over time; ignore what you do not recognise rather than failing on it.

  • Errors are always the same shape:

    {"error": "a sentence describing what went wrong"}

    The message is written for a human to read. Do not match on its text.

  • Status codes carry the meaning: 400 malformed request, 401 no or expired session, 403 missing privilege, missing or wrong CSRF token, or a disabled preview, 404 unknown object, 409 a conflicting state (already exists, wrong power state, admission control refused), 413 body too large, 429 rate-limited (sign-in), 500 controller fault, 502 the host behind a proxied call could not be reached, 503 a subsystem is not wired on this controller.

  • A proxied host's own refusal reaches you intact. Many endpoints under /api/v1/hosts/{id}/... are forwarded to that host. When the host refuses — a datastore is in use, a VM is in the wrong state, a lease fence is held — its status code and its own sentence are what you receive, not a generic controller error. 502 means the controller could not reach the host at all, which is a different problem.

  • Timestamps are RFC 3339 strings in UTC unless a field name says otherwise.

  • Sizes are bytes, and the field name says so (memory_total_bytes). Memory on VM-shaped objects is mebibytes (memory_mib).

  • Identifiers: hosts and clusters carry opaque string ids. Virtual machines are addressed by their libvirt uuid. Datastores are addressed by a stable key (see datastores) — never by name.

  • Paging. List endpoints that can grow take ?limit=. Each states its default and cap. There is no cursor paging in this release.

  • Treat a missing or null array as empty. Collections are normally serialized as [], but a client that also accepts null will never break on one.

Authorization

Every route is bound to one privilege. Roles are sets of privileges; users and directory groups are bound to roles. A principal whose role does not include the privilege gets:

{"error": "you do not have permission to do this (vm.delete)"}

with status 403. The denial is recorded in the audit log.

The full privilege set:

Privilege Covers
vm.view See virtual machines, their detail, snapshots, placement and tasks
vm.create Create a VM, deploy from a template, create a Kubernetes cluster
vm.power Start, stop, reset, suspend, resume
vm.edit Reconfigure: CPU/memory, disks, NICs, media, snapshots, template flag, folder
vm.delete Delete a virtual machine
vm.migrate Live, cold and storage migration; host evacuation
vm.console Open the graphical or serial console
host.view See hosts, their hardware, health, services, capacity and alarms
host.manage Add/remove hosts, power and maintenance, host configuration, certificates, host accounts, updates, the controller audit log and the fleet event tail
cluster.view See clusters, HA state and readiness, CPU compatibility
cluster.manage Create/delete clusters, membership, cluster switches, cluster CPU profile
cluster.ha Arm or disarm high availability; approve or reject a pending failover
datastore.view See datastores, browse files, read the content library
datastore.manage Add/remove datastores, upload/delete files, manage the library, reclaim orphans
network.view See switches, networks and port groups
network.manage Create/modify switches, port groups, uplinks, service adapters
admin.users Local accounts, role bindings, certificates, backup/restore, support bundle, controller updates
admin.roles Create, edit and delete custom roles
admin.directory Configure LDAP/AD and directory role bindings

GET /api/v1/authz/catalog serves this catalog from the controller, so a client never has to hard-code it.

Two endpoints carry no privilege by design, and both are scoped so they can reveal nothing about the fleet: GET /api/v1/auth/me (the caller's own identity — gating it would refuse the very call a zero-privilege principal makes to discover it has no privileges) and GET /api/v1/settings/features (which previews this build offers, which the console must know before its first render).

Preview features

Eight capabilities ship as previews: built, in the binary, and off by default. An endpoint belonging to a disabled preview answers:

{"error": "…", "experimental": "cluster_services"}

with status 403 — distinguishable from a privilege denial by the experimental key. GET /api/v1/settings/features reports which are enabled on this controller. Full detail is in the preview features guide.

Preview endpoints are marked (preview: flag) throughout this reference and are collected in preview feature endpoints. Do not build a client that depends on one without reading that guide first.

Stability

Surface Status Notes
/api/v1/... on SimpleVirt-Command Stable. Integrate against this. Additive changes only within a major version: new endpoints, new response fields. Existing fields keep their meaning.
/api/v1/agent/... on SimpleVirt-Command Reserved. Not present on the controller; see below for what it is on a host.
/api/v1/agent/... on a SimpleVirt host Internal. Do not call. The controller-to-host channel. Its shapes change with the pair of builds at either end and carry no compatibility promise.
/api/v1/... on a SimpleVirt host Internal. The host's own recovery console. A host is meant to be operated through the controller.
/api/v1/cluster/... on a SimpleVirt host Internal. Loopback-only, token-guarded cluster-services control surface.

The rule for a client is simple: talk to SimpleVirt-Command, over /api/v1, and nothing else. If an operation appears to need a host directly, it is available on the controller — every host-backed capability is proxied under /api/v1/hosts/{id}/....


Resources

Session and discovery

Method Path Privilege Description
POST /api/v1/auth/login none Sign in. Body {username, password}. Sets the session cookie, returns {username, csrf_token}. Rate-limited per source address.
POST /api/v1/auth/logout session Destroy the session and clear the cookie. Requires the CSRF header.
GET /api/v1/auth/me session The caller's own {username, csrf_token, privileges[], rbac_active, can_manage_access}. The one call that recovers a CSRF token without re-authenticating.
GET /api/v1/version none The controller's build: {version, flavor, release, commit, build_date, dirty}. Readable with no session — the sign-in page renders it before anyone has authenticated.
GET /api/v1/settings/features session Which preview features this controller offers: a name, a label and a boolean for each. No fleet data.

Inventory and dashboard

Method Path Privilege Description
GET /api/v1/dashboard vm.view Fleet totals: hosts, hosts up, clusters, VMs, VMs running, cores, memory total/free, average CPU percent, and provisioned vCPU/memory.
GET /api/v1/vms vm.view Every virtual machine across every host — see virtual machines.
GET /api/v1/hosts host.view Every host — see hosts.
GET /api/v1/clusters cluster.view Every cluster — see clusters.
GET /api/v1/datastores datastore.view Every datastore across the fleet — see datastores.
GET /api/v1/networking network.view The fleet networking rollup: switches, networks and port groups per host, with health.
GET /api/v1/placement vm.view Which datastore each VM's disks actually sit on, fleet-wide. Answers from cache and rebuilds in the background, so it never blocks on a slow host. Join to datastores on key.
GET /api/v1/guest-tools/catalog vm.view The SimpleVirt Guest Tools version bundled with this release, its protocol version and supported-OS matrix. Derived from the build; never fetched from the internet.

Aggregated inventory is read from the controller's poll cache, not from the hosts. A read never blocks on an unreachable host, and objects on a host that has stopped answering are still returned — carried forward from the last successful poll and flagged. This is deliberate: a VM must not appear to vanish because its host went quiet. Two fields tell you what you are looking at:

  • stale: true — this reading is a memory, not a measurement.
  • host_reachable / host_last_seen — whose memory, and how old.

Treat a stale object as present-but-unconfirmed, and say so to your user.

Virtual machines

GET /api/v1/vms returns one object per VM, deduplicated to exactly one row per UUID even mid-failover:

[
  {
    "uuid": "c0ffee…", "name": "web-01", "state": "running",
    "vcpus": 4, "memory_mib": 8192,
    "persistent": true, "is_template": false, "os_variant": "debian",
    "usage": {"cpu_mhz": 412, "memory_bytes": 5100273664, "storage_bytes": 21474836480},
    "lease_guarded": true,
    "host_id": "h-1", "host_name": "simplevirt-01", "cluster_id": "c-1",
    "folder_id": "f-2",
    "host_reachable": true, "host_liveness": "online"
  }
]

state is one of running, stopped, paused, shutting_down, crashed, suspended, unknown. usage is a live sample and is absent when there is no current reading (a stopped VM, or before the first sample) — absent means unknown, and a client must render it as unknown rather than as zero.

Individual VMs are addressed through their host. {id} is the host id, {uuid} the VM's UUID.

Method Path Privilege Description
GET /api/v1/hosts/{id}/vms/{uuid} vm.view Full detail: disks, NICs, current media, VNC port, passed-through PCI/USB devices, CPU topology and pinning, the VM's actual guest CPU definition, and the two guest-side views described below.
POST /api/v1/hosts/{id}/vms vm.create Create a VM on this host. Body is the create request described below.
POST /api/v1/clusters/{id}/vms vm.create Create a VM in a cluster, letting the controller place it on an eligible member. Same body.
PATCH /api/v1/hosts/{id}/vms/{uuid} vm.edit Reconfigure: name, vCPUs, memory, description, autostart, boot order, CPU topology, pinning.
DELETE /api/v1/hosts/{id}/vms/{uuid} vm.delete Delete the VM. Disk deletion is requested explicitly; read the response.
POST /api/v1/hosts/{id}/vms/{uuid}/actions vm.power Power operations. Body {"action": "…"}.
POST /api/v1/hosts/{id}/vms/{uuid}/deploy vm.create Deploy a new VM from this template. Body {name, datastore?, start}.
POST /api/v1/hosts/{id}/vms/{uuid}/template vm.edit Mark or unmark the VM as a template.
GET /api/v1/vms/{uuid}/cpu-profile vm.view The CPU model this VM is running under and how it relates to its cluster's compatibility profile.
GET /api/v1/hosts/{id}/vms/{uuid}/os/detect vm.view Best-effort guest OS detection for the icon and for guest-tools eligibility.

Two guest-side facts on a VM's detail, easily confused. guest_agent is the QEMU guest agent: connected, disconnected, or absent when the VM is stopped or has no agent channel — absent means unknown, not no. It is the single fact that decides what a graceful shutdown can actually do, because without it libvirt presses the virtual ACPI power button and a guest that ignores ACPI (no OS installed yet, a minimal image, a Windows lock screen) simply keeps running. guest_tools is the separate first-party SimpleVirt Guest Tools view: whether simplevirt-guestd is running and, when it is, the guest's own report of its OS, hostname, uptime, capabilities and IP addresses. It is best-effort and absent for a stopped VM. The two coexist and neither implies the other.

configured_vcpus and configured_memory_mib appear only when the persistent config differs from what the guest is currently running — CPU and memory can be changed while a VM runs, and the change takes effect on the next boot. Without these, a just-saved change would look as though it had been silently discarded.

Power actions — the action field takes exactly one of:

Action Effect
start Power on
shutdown Graceful shutdown via ACPI or the QEMU guest agent
forcestop Immediate power off — the equivalent of pulling the cord
reboot Graceful restart
reset Immediate reset
suspend Suspend to host memory
resume Resume from suspend
guest-shutdown OS-native shutdown coordinated by SimpleVirt Guest Tools. Never escalates to a forced power off.
guest-restart OS-native restart coordinated by SimpleVirt Guest Tools

Anything else is 400 with the accepted list in the message.

Creating a VM — POST /api/v1/hosts/{id}/vms. The commonly used fields:

{
  "name": "web-01",
  "vcpus": 4, "memory_mib": 8192, "disk_gib": 80,
  "datastore": "shared-nfs", "disk_type": "thin",
  "network": "vm-network",
  "os_variant": "debian",
  "start": true, "autostart": false
}

Install media is attached either as iso (a basename from the host's ISO library) or as iso_datastore + iso_path (a path inside a datastore) — the two are mutually exclusive. A VM can instead be cloned from an existing volume with source_datastore + source_volume, which is how a prepared cloud image is used as a base. cloud_init_user_data and cloud_init_network attach a NoCloud seed for a cloud-init-capable guest. mac fixes the NIC's hardware address when something outside the VM already knows it — a DHCP reservation, a MAC-bound licence — and SimpleVirt does not check it is unique on the network, because it cannot see the network. boot_order takes an ordered list of hd, cdrom, network; cpu_sockets × cpu_cores × cpu_threads must equal vcpus when all three are given; cpu_set pins vCPUs to a host CPU range.

Create is refused with 409 when cluster admission control cannot reserve the capacity — the HA failover reserve is enforced at create and at power-on.

Disks, NICs, media and snapshots

Method Path Privilege Description
POST /api/v1/hosts/{id}/vms/{uuid}/disks vm.edit Attach a new or existing disk.
POST /api/v1/hosts/{id}/vms/{uuid}/disks/{dev}/resize vm.edit Grow a disk. {dev} is the guest target, e.g. vda.
DELETE /api/v1/hosts/{id}/vms/{uuid}/disks/{dev} vm.edit Detach a disk. On a running VM the detach is asynchronous and the response reports what was actually confirmed.
POST /api/v1/hosts/{id}/vms/{uuid}/nics vm.edit Attach a NIC to a network or port group.
DELETE /api/v1/hosts/{id}/vms/{uuid}/nics/{mac} vm.edit Detach the NIC with this MAC address.
POST /api/v1/hosts/{id}/vms/{uuid}/cdrom vm.edit Change or eject the CD/DVD medium. Attaches a drive if the VM has none.
GET /api/v1/hosts/{id}/vms/{uuid}/snapshots vm.view List snapshots with their ages.
POST /api/v1/hosts/{id}/vms/{uuid}/snapshots vm.edit Create a snapshot.
POST /api/v1/hosts/{id}/vms/{uuid}/snapshots/{name}/revert vm.edit Revert to a snapshot.
DELETE /api/v1/hosts/{id}/vms/{uuid}/snapshots/{name} vm.edit Delete a snapshot.
POST /api/v1/hosts/{id}/vms/{uuid}/guest-tools/install vm.edit Attach the SimpleVirt Guest Tools medium and begin installation.

Snapshots are a rollback convenience, not a backup — see backup responsibility.

Migration

Method Path Privilege Description
GET /api/v1/hosts/{id}/vms/{uuid}/migrate-targets vm.view Every other member of the VM's cluster, each with a compatibility verdict and a reason when it is not eligible. Call this to populate a destination picker.
POST /api/v1/hosts/{id}/vms/{uuid}/migrate vm.migrate Live-migrate a running VM to another host.
POST /api/v1/hosts/{id}/vms/{uuid}/cold-migrate vm.migrate Move a stopped VM, copying disks when storage is not shared.
POST /api/v1/hosts/{id}/vms/{uuid}/storage-migrate vm.migrate Move the VM's disks to another datastore, with the VM running or stopped.
POST /api/v1/hosts/{id}/evacuate vm.migrate Live-migrate every running VM off a host onto surviving members.

All four run as tracked tasks with progress. The call returns once the operation is accepted; watch the task for completion.

Virtual machine console

The console is a two-step, ticketed WebSocket proxy: the controller mints a short-lived ticket bound to your session, the VM and the host, and the browser (or client) then opens the WebSocket with it.

Method Path Privilege Description
GET /api/v1/hosts/{id}/vms/{uuid}/console vm.console Mint a console ticket. Returns {"ticket": "…"}.
GET /api/v1/hosts/{id}/vms/{uuid}/console/ws vm.console WebSocket: the VM's graphical (VNC) console, proxied to the host.
GET /api/v1/hosts/{id}/vms/{uuid}/serial/ws vm.console WebSocket: the VM's serial (text) console on ttyS0. Requires a getty in the guest.

Tickets are single-use and expire in seconds. Both sockets carry a keepalive ping on each leg, so a half-open client tab cannot leak a connection to the host.

Folders

Folders are a controller-side organizational grouping for virtual machines: a flat set of named buckets, not a hierarchy — a folder has no parent and cannot contain another folder. They exist only in the controller and change nothing on any host. Membership is keyed by the VM's UUID, so it survives the VM migrating between hosts.

Method Path Privilege Description
GET /api/v1/folders vm.view Every folder, name-sorted.
POST /api/v1/folders vm.edit Create a folder.
PATCH /api/v1/folders/{id} vm.edit Rename a folder.
DELETE /api/v1/folders/{id} vm.edit Delete a folder. VMs in it become unfiled; nothing is deleted.
PUT /api/v1/vms/{uuid}/folder vm.edit File a VM into a folder, or unfile it.

Hosts

GET /api/v1/hosts returns one object per registered host:

[
  {
    "id": "h-1", "name": "simplevirt-01", "address": "10.0.0.11:8443",
    "cluster_id": "c-1", "cluster_name": "production",
    "status": "online", "liveness": "online", "reachable": true,
    "last_seen": "2026-09-17T09:14:22Z", "info_at": "2026-09-17T09:14:22Z",
    "vms": 12, "vms_running": 9,
    "cpu_cores": 48, "cpu_percent": 18.4,
    "memory_total_bytes": 412316860416, "memory_free_bytes": 180388626432,
    "version": "1.0.0+…", "cpu_model": "…", "domain_type": "kvm",
    "maintenance_mode": false
  }
]

status is connecting, online, degraded or unreachable. While connecting, connect_phase narrates progress (contacting, waiting-agent, cert) without leaking a raw error the operator cannot act on; once a host escalates to unreachable, fail_class, error and terminal say what is wrong and whether it will clear on its own. Capacity fields carry omitempty, so absent means never read — gate them on info_at, not on the value.

Method Path Privilege Description
GET /api/v1/hosts host.view Every registered host.
GET /api/v1/hosts/{id} host.view One host.
POST /api/v1/hosts host.manage Add a host. Body {name?, address, username, password, cluster_id?}. The controller pairs with it, pins its certificate on first contact, and installs an agent token; the password is used for the pairing exchange and is not stored.
PATCH /api/v1/hosts/{id} host.manage Correct the address, the display name, or both. A name-only correction touches nothing on the wire. An address correction must first prove the same host answers there.
DELETE /api/v1/hosts/{id} host.manage Remove a host from the controller.
POST /api/v1/hosts/{id}/cluster host.manage Assign the host to a cluster, or remove it from one.
POST /api/v1/hosts/{id}/repoll host.view Ask the controller to poll this host now instead of waiting for the next cycle.
POST /api/v1/hosts/{id}/maintenance host.manage Enter or leave maintenance mode. Entering blocks VM create and power-on and can gracefully stop or suspend running VMs.
GET /api/v1/hosts/{id}/maintenance/preview host.view Read-only: plans the evacuation of every running VM and reports the storage impact, so the consequences are visible before the act.
POST /api/v1/hosts/{id}/planned-reboot host.manage Reboot the host as a planned outage, so HA treats the absence as expected rather than as a failure.
GET /api/v1/hosts/{id}/offline-impact host.view What taking this host offline would cost right now — proxied live from the host.

Host certificates and identity

Method Path Privilege Description
GET /api/v1/hosts/{id}/certificate host.manage The host's current serving certificate and whether SimpleVirt manages it.
PUT /api/v1/hosts/{id}/certificate host.manage Install an operator-supplied certificate and key on the host.
POST /api/v1/hosts/{id}/certificate/regenerate host.manage Have the host regenerate its self-signed certificate.
GET /api/v1/hosts/{id}/certificate/pin host.manage Read the fingerprint the host is presenting now, over a bare handshake that sends no token. It pins nothing — an operator compares it against the host's own certificate before approving.
POST /api/v1/hosts/{id}/certificate/pin host.manage Re-pin the host to a new fingerprint, optionally correcting the address in the same act.

The controller pins each host's certificate on first contact and refuses to talk to a changed one. That is the intended behaviour after a certificate is reissued: read the new fingerprint, verify it against the host, then re-pin.

Host hardware, services and time

Method Path Privilege Description
GET /api/v1/hosts/{id}/health host.view Live health: services, storage, networking, with an overall verdict.
GET /api/v1/hosts/{id}/services host.view The host's managed system services and their state.
POST /api/v1/hosts/{id}/services/{unit} host.manage Start, stop or restart one service.
GET /api/v1/hosts/{id}/disks host.view Physical block devices, with what each is used for.
GET /api/v1/hosts/{id}/pci host.view PCI devices, for the passthrough picker.
GET /api/v1/hosts/{id}/usb host.view USB devices, for the passthrough picker.
POST /api/v1/hosts/{id}/pci/passthrough host.manage Bind or unbind a PCI device for passthrough.
GET /api/v1/hosts/{id}/cpu-capabilities host.view The host's CPU model and feature set, as the cluster compatibility profile uses it.
GET /api/v1/hosts/{id}/time host.view Clock, timezone and NTP synchronization state.
POST /api/v1/hosts/{id}/ntp host.manage Set the host's NTP servers.
POST /api/v1/hosts/{id}/power host.manage Reboot or power off the host.
GET /api/v1/hosts/{id}/events host.manage The host's own audit tail.
GET /api/v1/hosts/{id}/users host.manage The host's local operating-system accounts.
POST /api/v1/hosts/{id}/users host.manage Add a host account.
DELETE /api/v1/hosts/{id}/users/{name} host.manage Remove a host account.
GET /api/v1/hosts/{id}/license host.view The subscription entitlement recorded on this host.
POST /api/v1/hosts/{id}/license host.manage Activate a subscription token on the host.
DELETE /api/v1/hosts/{id}/license host.manage Deactivate it.

The subscription is a commercial entitlement, not a software licence, and it gates nothing in the product — see licensing.

Host-local storage and media

These act on one host's view. Fleet-wide and cluster storage is under datastores.

Method Path Privilege Description
GET /api/v1/hosts/{id}/datastores datastore.view Datastores this host has mounted, with capacity. Raw and complete; the two pools of a SimpleVirt HCI datastore each carry the additive hci_role / hci_name — see one HCI datastore.
POST /api/v1/hosts/{id}/datastores datastore.manage Add a datastore to this host. Block types (iSCSI, FC) are refused unless that preview is enabled.
DELETE /api/v1/hosts/{id}/datastores/{name} datastore.manage Remove a datastore from this host. Refused with 409 for SimpleVirt HCI storage (the RBD pool and the CephFS files pool): remove that from the SimpleVirt HCI tab.
GET /api/v1/hosts/{id}/datastores/{name}/files datastore.view Browse a directory.
PUT /api/v1/hosts/{id}/datastores/{name}/files datastore.manage Upload a file. Streams; body is the file, not JSON.
DELETE /api/v1/hosts/{id}/datastores/{name}/files datastore.manage Delete a file.
POST /api/v1/hosts/{id}/datastores/{name}/mkdir datastore.manage Create a directory.
GET /api/v1/hosts/{id}/datastores/{name}/download datastore.view Download a file. Streams raw bytes — this hands back the contents of a virtual disk, which is why it carries the same privilege as any other view of that datastore.
GET /api/v1/hosts/{id}/isos datastore.view The host's ISO library.
POST /api/v1/hosts/{id}/isos datastore.manage Upload an ISO. Streams.
DELETE /api/v1/hosts/{id}/isos/{name} datastore.manage Delete an ISO.

Host networking

Method Path Privilege Description
GET /api/v1/hosts/{id}/networks network.view Networks available to VMs on this host.
POST /api/v1/hosts/{id}/switches network.manage Create a logical switch.
PATCH /api/v1/hosts/{id}/switches/{name} network.manage Modify a switch — uplinks, MTU, teaming.
DELETE /api/v1/hosts/{id}/switches/{name} network.manage Delete a switch.
POST /api/v1/hosts/{id}/portgroups network.manage Create a port group on a switch.
DELETE /api/v1/hosts/{id}/portgroups/{name} network.manage Delete a port group.
POST /api/v1/hosts/{id}/serviceadapters network.manage Create a service adapter (a host IP interface for management, migration or storage).
POST /api/v1/hosts/{id}/serviceadapters/{name}/test network.manage Probe an adapter's connectivity.
DELETE /api/v1/hosts/{id}/serviceadapters/{name} network.manage Delete a service adapter.

Host updates

Method Path Privilege Description
GET /api/v1/hosts/{id}/update/status host.view What is staged on this host, and the state of an upgrade in progress.
POST /api/v1/hosts/{id}/update host.manage Stage a signed image on the host by streaming it through the controller on this request; body is the file. Kept for API clients — the console uploads once to /api/v1/lifecycle/hypervisor instead. 409 while a background staging job for this host is running.
DELETE /api/v1/hosts/{id}/update host.manage Clear the host's staged image without applying it. 409 while a background staging job for this host is running.
POST /api/v1/hosts/{id}/update/apply host.manage Apply the staged image. The host reboots into its own in-place upgrade. 409 while a background staging job for this host is running.

An image is the only upgrade artifact. Stage first, apply second: applying verifies the staged image before it touches the running system.

Clusters

Method Path Privilege Description
GET /api/v1/clusters cluster.view Every cluster with pooled capacity, member counts, HA and distributor state.
GET /api/v1/clusters/{id} cluster.view One cluster.
POST /api/v1/clusters cluster.manage Create a cluster.
PATCH /api/v1/clusters/{id} cluster.manage Rename or reconfigure.
DELETE /api/v1/clusters/{id} cluster.manage Delete a cluster. Members become unclustered; nothing is destroyed.

A cluster object carries pooled cpu_cores, memory_total_bytes, memory_free_bytes, a member-average cpu_percent, and vcpus_provisioned / memory_provisioned_bytes — the configured resources of running VMs on reachable members. Provisioned figures may exceed physical capacity; that excess is the overcommit ratio. A member that could not be read contributes nothing, which makes these a floor rather than a total.

datastores on a cluster object is desired state — what every member is supposed to mount. It says nothing about what any host has actually mounted; that costs a live call per host and lives at GET /api/v1/clusters/{id}/datastores. Do not render the desired list as "the cluster has these datastores".

Cluster storage

Method Path Privilege Description
GET /api/v1/clusters/{id}/datastores datastore.view What each member has actually mounted, per host, live.
POST /api/v1/clusters/{id}/datastores datastore.manage Declare a datastore for the cluster and fan the mount out to every member.
POST /api/v1/clusters/{id}/datastores/{name}/mount datastore.manage Re-drive the mount on members that do not have it.
DELETE /api/v1/clusters/{id}/datastores/{name} datastore.manage Remove a cluster datastore from every member.
GET /api/v1/clusters/{id}/storage/backends datastore.view The storage backends this cluster can use, with capabilities and default failures-to-tolerate.
GET /api/v1/clusters/{id}/storage/protection datastore.view Backend-authoritative: what a chosen ?ftt= resolves to on ?hosts= contributing hosts — copies, minimum writes, reduced-resilience flag, usable-capacity divisor. Read-only, and the source of truth a UI should render rather than recomputing the policy itself.

Cluster networking

Method Path Privilege Description
GET /api/v1/clusters/{id}/switches network.view Logical distributed switches declared for this cluster, with per-host realization state.
POST /api/v1/clusters/{id}/switches network.manage Declare a distributed switch.
PATCH /api/v1/clusters/{id}/switches/{name} network.manage Modify it.
DELETE /api/v1/clusters/{id}/switches/{name} network.manage Delete it.
POST /api/v1/clusters/{id}/switches/{name}/hosts/{host} network.manage Assign a member to the switch — realize it there.
DELETE /api/v1/clusters/{id}/switches/{name}/hosts/{host} network.manage Unassign a member.
POST /api/v1/clusters/{id}/switches/{name}/remediate network.manage Re-converge members whose realization has drifted from the declaration.

CPU compatibility

A cluster can be given a CPU compatibility profile so a VM started on one member can migrate to any other — the guest is presented a common feature baseline instead of whatever its current host happens to have.

Method Path Privilege Description
GET /api/v1/clusters/{id}/cpu-compatibility cluster.view The active profile, or none.
PUT /api/v1/clusters/{id}/cpu-compatibility cluster.manage Apply a profile.
DELETE /api/v1/clusters/{id}/cpu-compatibility cluster.manage Disable it.
POST /api/v1/clusters/{id}/cpu-compatibility/precheck cluster.view Dry-run a profile change: what it would mean for every member and every VM. Read-only despite being a POST — it takes a candidate profile in the body.
GET /api/v1/clusters/{id}/cpu-compatibility/hosts cluster.view Per-member capability and whether each satisfies the profile.
GET /api/v1/clusters/{id}/cpu-compatibility/vms cluster.view Per-VM: which profile each running VM is actually on, and which need a power cycle to pick up a change.

High availability

HA restarts a dead host's VMs on surviving members. It fires only on real host death, never on a planned reboot, and it is gated on a readiness check plus a storage fence — see the operations guide.

Method Path Privilege Description
POST /api/v1/clusters/{id}/ha cluster.ha Arm or disarm HA on a cluster, and set its mode and admission-control reserve.
GET /api/v1/clusters/{id}/ha/readiness cluster.view Whether the cluster actually satisfies what HA requires — shared storage, fencing, network eligibility — with a reason per failed check. Read this before arming; arming a cluster that is not ready is how HA becomes a surprise.
GET /api/v1/clusters/{id}/ha/protection cluster.view Computed protection status per evaluated VM, plus whether the protection loop is running at all (so a client can show "evaluating" rather than an empty state that reads as "nothing to protect").
GET /api/v1/clusters/{id}/ha/pending cluster.view Failover plans a confirm-mode cluster has computed and is waiting for an operator to approve. Empty in automatic mode, and empty when nothing is failing.
POST /api/v1/clusters/{id}/ha/pending/{pid}/approve cluster.ha Approve a pending failover plan.
POST /api/v1/clusters/{id}/ha/pending/{pid}/reject cluster.ha Reject it.
GET /api/v1/ha/state cluster.view Fleet-wide HA state: authority, per-host runtime ownership, fencing.
GET /api/v1/ha/vms/{uuid}/state cluster.view One VM's HA state: who owns its runtime, whether it is lease-guarded, where it was recovered.
GET /api/v1/ha/events cluster.view The HA event history — every transition, durable across a controller restart. Takes ?limit=.
GET /api/v1/ha/events/stream cluster.view The same events as Server-Sent Events, live.

These four /api/v1/ha/... endpoints are observability only. Reading them drives no recovery decision; they report transitions the backend has already committed.

Datastores and files

GET /api/v1/datastores returns the fleet's datastores, merged across the hosts that mount them:

{
  "datastores": [
    {
      "key": "ds-a1b2", "id": "a1b2…", "grouped": true,
      "name": "shared-nfs", "type": "nfs", "source": "10.0.0.5:/export/vm",
      "shared": true, "host_count": 3,
      "capacity_bytes": 10995116277760,
      "allocated_bytes": 4398046511104,
      "available_bytes": 6597069766656
    }
  ],
  "hosts": [{"host_id": "h-1", "host_name": "simplevirt-01", "…": "…"}]
}

Two things matter for a client:

  • key is the join field. It is the datastore's stable address — the storage-stamped id when the hosts could report one, otherwise a host-private key. Other feeds, notably GET /api/v1/placement, join on key. Never join on name: a datastore can be renamed, and two hosts sharing one datastore can call it different things (names_differ).
  • Capacity is one host's report, never a sum. Summing a shared filesystem across the hosts that mount it would multiply one datastore's size by its popularity. grouped: false means the entry describes exactly one host's pool and was not merged with anything.
Method Path Privilege Description
GET /api/v1/datastores datastore.view Every datastore across the fleet, merged by identity. A SimpleVirt HCI cluster's two pools are presented as one composite datastore; ?flat=1 returns the unmerged list.
GET /api/v1/datastores/{key}/files datastore.view Browse a directory. ?path= is datastore-relative.
PUT /api/v1/datastores/{key}/files datastore.manage Upload a file. Streams.
DELETE /api/v1/datastores/{key}/files datastore.manage Delete a file.
POST /api/v1/datastores/{key}/mkdir datastore.manage Create a directory.
GET /api/v1/datastores/{key}/download datastore.view Download a file. Streams raw bytes.
GET /api/v1/datastores/{key}/orphans datastore.view Disk files on this datastore that no VM in the fleet references. A report, not an instruction.
POST /api/v1/datastores/{key}/orphans/reclaim datastore.manage Delete a reported orphan. Every gate is re-run at the instant of deletion — a fresh fleet probe, the age and type qualification, and the cluster-wide in-use question — because the report the operator is looking at was computed earlier and a disk can have been attached since.

One HCI datastore

SimpleVirt HCI provisioning creates two storage pools per cluster: an RBD pool that holds VM disks (libvirt name hci-<ceph pool>, for example hci-simplevirt-hci) and a CephFS pool that holds ISOs and other files (hci-fs). Operators see one datastore, named after the Ceph pool (simplevirt-hci). The merge is presentation only, performed by the controller at the very end of GET /api/v1/datastores: no pool, mount path, Ceph object or libvirt name is renamed or moved, and every host's own list (GET /api/v1/hosts/{id}/datastores) stays raw.

A merged row is the RBD member's row plus these additive fields (a row that is not merged carries none of them):

{
  "key": "ceph-rbd:…", "id": "ceph-rbd:…", "name": "hci-simplevirt-hci", "type": "rbd",
  "shared": true, "host_count": 3,
  "capacity_bytes": 161061273600, "allocated_bytes": 32212254720, "available_bytes": 128849018880,
  "composite": true,
  "display_name": "simplevirt-hci",
  "has_disks": true, "has_files": true,
  "disks_name": "hci-simplevirt-hci",
  "files_key": "ceph-fs:…", "files_name": "hci-fs", "files_state": "ok",
  "member_keys": ["ceph-rbd:…", "ceph-fs:…"],
  "members": [
    {"role": "disks", "key": "ceph-rbd:…", "id": "ceph-rbd:…", "name": "hci-simplevirt-hci", "type": "rbd",
     "keys": ["ceph-rbd:…"], "hosts_present": 3, "hosts_active": 3, "hosts_expected": 3, "state": "ok"},
    {"role": "files", "key": "ceph-fs:…", "id": "ceph-fs:…", "name": "hci-fs", "type": "cephfs",
     "keys": ["ceph-fs:…"], "hosts_present": 3, "hosts_active": 3, "hosts_expected": 3, "state": "ok"}
  ],
  "hosts": [
    {"host_id": "…", "host_name": "…", "name": "hci-simplevirt-hci", "role": "disks", "active": true, "…": "…"},
    {"host_id": "…", "host_name": "…", "name": "hci-fs", "role": "files", "active": true, "target_path": "/var/lib/simplevirt-hcifs", "…": "…"}
  ]
}
  • key, id and type are the RBD member's, so every existing join (the placement feed, a VM disk's recorded datastore, HA, deep links, #/storage/datastore/<key> bookmarks) keeps working. name stays the RBD libvirt pool name; show display_name when present. member_keys lists every key that resolves to this datastore (the RBD key, the CephFS key, and the host-private local: keys of inactive member pools), so a link made against any of them still resolves.
  • hosts carries one entry per (host, member), and host_count counts distinct hosts. Each entry has role ("disks" for the RBD pool, "files" for the CephFS pool; the field exists only on a composite's entries), the pool's own libvirt name on that host, and its own active flag, so a console can say which host lacks WHICH pool. An inactive member pool has no identity; its host is folded in as an entry with active: false (and counted in members[].hosts_present versus hosts_active), never shown as a third row and never counted in host_count: a host that has only inactive pools is listed, not counted.
  • members[].keys is every key a VM disk of that member may be recorded under: the member's own key plus the host-private local: keys of its absorbed inactive pools. It is always an array, never null; the members' keys together are exactly member_keys.
  • Capacity is the RBD member's usable figure only (ceph df), never a sum with — and never a fallback to — the CephFS filesystem figures, which an unmounted host fills with a bare local directory's.
  • All file operations use files_key, not key: browse, upload, mkdir, download and delete go to /api/v1/datastores/{files_key}/…. VM disks are created on key. The value an ISO picker sends as the datastore is files_name (hci-fs).
  • files_state is ok (the files pool is active on every member host), partial (not active everywhere, including a pool that is defined but down on every host), absent (no file member at all) or unknown (a host of the cluster could not be read, so neither "ok" nor "absent" can be claimed). members[].state is ok, partial, inactive, absent or unknown on the same terms.
  • When rows are NOT merged. Rows fold only within one registry cluster, only when the cluster has exactly one RBD identity (one Ceph pool name) and at most one CephFS identity, only when the CephFS pool is mounted at the fixed HCI path (an operator's own CephFS pool elsewhere is never swallowed), and never when the two report different Ceph cluster ids. Any ambiguity leaves today's separate rows. A cluster with only an RBD pool is a composite with has_files: false; one with only a CephFS pool is a single row named after the file member with has_disks: false. When the RBD pool is inactive on every host it has no identity, so nothing is merged: the rows stay exactly as before the merge existed (never a files-only composite beside phantom block rows).
  • ?flat=1 returns the unmerged list (one row per storage identity, as before). The response's hosts[] status entries additionally carry cluster_id.

On GET /api/v1/hosts/{id}/datastores, the entries that belong to an HCI pair are annotated additively with hci_role ("disks" for the RBD pool, "files" for the CephFS pool) and hci_name (the logical name). Both fields are omitted for every other entry, and no entry is merged, renamed or dropped.

DELETE /api/v1/hosts/{id}/datastores/{name} is refused with 409 ("This is SimpleVirt HCI storage. Manage or remove it from the SimpleVirt HCI tab.") for the RBD pool and the CephFS files pool, by both the controller and the host. NFS, local-directory and default-pool rules are unchanged. The host's datastore in-use check now also counts a VM whose disk is an RBD network disk in the pool's Ceph pool.

Content library

The content library is the catalog of base images, ISOs and templates that VM creation and provisioning deploy from. It is backed by a shared datastore so items stay reachable cluster-wide.

Method Path Privilege Description
GET /api/v1/library datastore.view Library configuration and an overview.
PUT /api/v1/library datastore.manage Set the shared datastore that backs the library.
GET /api/v1/library/items datastore.view The catalog.
POST /api/v1/library/items/upload datastore.manage Upload an item. Streams.
POST /api/v1/library/items/import datastore.manage Import an item from a URL the controller can reach.
DELETE /api/v1/library/items/{id} datastore.manage Delete an item.

The "ISO (install media)" item type is behind the library_iso_items preview: no code in this release can mount a library ISO into a VM, so importing one produces a catalog row and nothing more. See preview features.

Fleet networking

Method Path Privilege Description
GET /api/v1/networking network.view The whole fleet's switches, port groups, uplinks and service adapters, with a health rollup per host and per switch.

Per-host and per-cluster network mutation is under host networking and cluster networking.

Tasks

Every mutating operation the controller performs runs as a task with a durable record, so the list survives a page reload and shows work started by other operators and by the controller itself.

Method Path Privilege Description
GET /api/v1/tasks vm.view Recent tasks, newest first. ?limit= defaults to 100 and caps at 500. A malformed limit is 400 rather than being quietly replaced with the default.
GET /api/v1/tasks/stream vm.view Server-Sent Events: an initial snapshot oldest-first, then one event per transition — begin, progress tick, terminal. Each data: line is one task record as JSON.

A task record:

{
  "id": "t-9", "task": "Migrate virtual machine", "target": "web-01",
  "target_kind": "vm", "host_id": "h-1", "initiator": "alice",
  "started": "2026-09-17T09:14:22Z", "finished": null,
  "status": "running", "progress": 42, "detail": "42% · 1.2 GiB left"
}

progress is absent for work with no measurable progress. target_kind is vm, host, cluster, datastore and so on. Prefer the stream: a long migration's real percent ticks at the host's cadence rather than aliasing to your poll interval, and a client that cannot hold the stream open still has GET /api/v1/tasks.

Events and audit

Three distinct feeds, easily confused:

Method Path Privilege Description
GET /api/v1/events host.manage The live merged fleet tail: recent activity from the controller and every host, in order. ?limit=. The response carries as_of, the contributing sources, and refreshing while a sweep is in flight.
GET /api/v1/audit host.manage The controller's own audit archive, paged. Every authenticated action, every authorization denial, every sign-in.
GET /api/v1/audit/export.csv host.manage The same archive as CSV, for an external log store.
GET /api/v1/hosts/{id}/events host.manage One host's audit tail, proxied from that host.

Alarms

Alarms are computed conditions — capacity thresholds, unreachable hosts, degraded storage, network-health rollups — that persist until the condition clears.

Method Path Privilege Description
GET /api/v1/alarms host.view Active alarms, a summary (total, critical, warning, unacknowledged) and notifications delivery statistics.
GET /api/v1/alarms/history host.view Alarm transitions, newest first. ?limit= defaults to 200. The answer to "was anything wrong last night?".
POST /api/v1/alarms/{id}/acknowledge host.manage Record that a named operator owns the alarm.

Acknowledging does not clear an alarm — only the condition going away does that, so an acknowledgement can never be mistaken for a fix. The notifications block reports whether a webhook receiver is configured and whether deliveries are being dropped; it never carries the endpoint URL or its secret.

Utilization metrics

Retained server-side history, so a client gets a series rather than a single live sample.

Method Path Privilege Description
GET /api/v1/hosts/{id}/metrics host.view This host's retained CPU and memory utilization history.
GET /api/v1/vms/{uuid}/metrics vm.view This VM's retained CPU, memory and storage history.

The live per-VM sample also rides the inventory poll, as usage on each object in GET /api/v1/vms — no extra request needed for a current reading.

Access control

Method Path Privilege Description
GET /api/v1/authz/catalog admin.users The full privilege catalog, so a role editor never hard-codes the list.
GET /api/v1/authz/roles admin.users All roles, built-in and custom, each with its privileges expanded to strings.
POST /api/v1/authz/roles admin.roles Create a custom role.
PUT /api/v1/authz/roles/{id} admin.roles Update a custom role.
DELETE /api/v1/authz/roles/{id} admin.roles Delete a custom role.
GET /api/v1/authz/bindings admin.users Which users and which directory groups are bound to which roles.
PUT /api/v1/authz/bindings/{name} admin.users Bind a user to a role.
PUT /api/v1/authz/group-bindings/{name} admin.users Bind a directory group to a role.
GET /api/v1/settings/users admin.users The controller's local accounts.
POST /api/v1/settings/users admin.users Create a local account.
DELETE /api/v1/settings/users/{name} admin.users Delete a local account.

Access control is default-deny: a principal with no binding has no privileges. The controller keeps at least one administrator binding at all times, so it cannot be locked out of itself. Local accounts remain the break-glass path — a directory outage never strands the controller.

Controller settings

Method Path Privilege Description
GET /api/v1/settings/certificate admin.users The controller's own serving certificate.
PUT /api/v1/settings/certificate admin.users Install a certificate and key.
POST /api/v1/settings/certificate/csr admin.users Generate a certificate signing request, including every name the controller answers to.
POST /api/v1/settings/certificate/reset admin.users Return to a self-signed certificate.
GET /api/v1/settings/directory admin.directory The LDAP/AD configuration. Bind credentials are never returned.
PUT /api/v1/settings/directory admin.directory Configure or disable the directory.
POST /api/v1/settings/directory/test admin.directory Test a directory configuration without saving it.
GET /api/v1/settings/security admin.users The controller's security posture: what is configured, what is default, what an operator should address. Admin-gated deliberately — the operators who can act on it are the ones who see it.
GET /api/v1/settings/features session Which previews are enabled. See preview features.

Lifecycle and updates

Method Path Privilege Description
GET /api/v1/lifecycle host.view The controller's own version and staged update, plus every host's version and reachability — what is current and what an update would move. jobs lists background uploads and stagings (active ones, and those finished in the last 30 minutes, newest first); hypervisor_image describes the retained hypervisor image, when there is one.
POST /api/v1/lifecycle/command admin.users Upload a controller image; the body is the file, and X-Upload-Filename (URI-encoded) names it. Answers 202 {"job": …} as soon as the whole body has arrived; verification against the controller flavor and staging then run on the controller, followed in jobs and Recent Tasks. Never applies. 409 while another controller upload is being received or verified; 507 when the controller's data disk cannot hold the image.
DELETE /api/v1/lifecycle/command admin.users Clear the staged controller image without applying it. 409 while a controller upload is being received or verified.
POST /api/v1/lifecycle/command/apply admin.users Apply the staged controller image. Answers 202: the controller cannot overlay its own running root, so it reboots into an unattended in-place upgrade. Hosts keep running throughout. 409 while a controller upload is being received or verified.
POST /api/v1/lifecycle/hypervisor host.manage Upload the hypervisor image once; the body is the file, X-Upload-Filename names it. Answers 202 {"job": …} when the body has arrived. The controller then verifies it, keeps it as the retained image, and stages it on every host that needs it — reachable, on an older version the image can upgrade from, and not already holding it — or only on the hosts named by repeated ?host=<id>. 409 while another hypervisor upload is being received or verified; 507 when the data disk cannot hold it.
POST /api/v1/lifecycle/hypervisor/stage host.manage Stage the retained hypervisor image on {"hosts": ["<id>", …]} without uploading it again — a retry, a newly added host, or a deliberate reinstall. Answers 202 {"jobs": […]}. Hosts already staging are skipped; 409 if every named host is. 404 when no image is retained.
DELETE /api/v1/lifecycle/hypervisor host.manage Remove the retained hypervisor image. 409 while an upload or a staging job could still be reading it. Images already staged on hosts are not touched.

Host-side staging and apply are under host updates.

Backup, restore and support

Method Path Privilege Description
GET /api/v1/backup admin.users Download a controller backup: a tar stream of the controller's state, including a database dump when running on PostgreSQL.
POST /api/v1/restore admin.users Restore from a backup archive. Streams the upload.
GET /api/v1/support-bundle admin.users Download a redacted diagnostic bundle (tar.gz): configuration, inventory and logs, built entirely in memory so a failure is a clean 500 rather than a half-downloaded archive.

This backs up the controller, not your virtual machines. What SimpleVirt protects and what you must protect yourself is set out in backup responsibility.


SimpleVirt HCI storage endpoints

SimpleVirt HCI is a supported, always-on storage tier — these endpoints are not gated by a preview flag and never answer 403 {"experimental": …}. (The …-preview paths are dry-run cost previews of a destructive operation, not experimental features.)

Method Path Privilege Description
GET /api/v1/clusters/{id}/hci/status datastore.view Pool health, capacity and protection state, plus the maintenance-aware rollup (state, headline, expected findings, hosts in maintenance, redundancy margin) and resilience ("what if this host goes offline", per host). A finding is marked expected only when planned maintenance explains the whole of it.
GET /api/v1/clusters/{id}/hci/objects datastore.view Virtual Objects: every virtual disk and the file store, the VM that owns each disk, where its copies live and whether they are all in place. Served from a cache; never a live storage call.
GET /api/v1/clusters/{id}/hci/objects/placement datastore.view One disk's sampled placement (?pool=&image=, validated against the cached inventory; cached 60 s, at most two samples at once per cluster).
POST /api/v1/clusters/{id}/hci/retest datastore.view Take a fresh storage sample and objects reading now (one per 3 s per cluster; read-only). An unreachable cluster is an error, not a quiet success.
GET /api/v1/clusters/{id}/hci/performance datastore.view Throughput and latency.
GET /api/v1/clusters/{id}/hci/provision-state datastore.view Where provisioning has got to.
GET /api/v1/clusters/{id}/hci/protection/plan datastore.view What the current protection policy resolves to.
POST /api/v1/clusters/{id}/hci/protection/remediate datastore.manage Re-converge protection.
GET /api/v1/clusters/{id}/hci/osd/{osd}/remove-preview datastore.view What removing one storage device would cost, before doing it.
POST /api/v1/clusters/{id}/hci/osd/{osd}/remove datastore.manage Safely remove one storage device: drain it, wait for the pool to re-converge, then purge the disk.
GET /api/v1/clusters/{id}/hci/host/{host}/decommission-preview datastore.view What decommissioning a host would cost, before doing it.
POST /api/v1/clusters/{id}/hci/host/{host}/decommission datastore.manage Safely retire a host: drain every disk it carries, then leave quorum.
POST /api/v1/clusters/{id}/hci/precheck datastore.manage Validate a cluster for HCI.
POST /api/v1/clusters/{id}/hci/remount-fs datastore.manage Re-drive the shared filesystem mount on members that lost it.
GET /api/v1/clusters/{id}/hci/network network.view The storage network HCI is using.
GET /api/v1/clusters/{id}/hci/network/candidates network.view Networks eligible to carry it.
POST /api/v1/clusters/{id}/hci/network/precheck network.manage Validate a candidate storage network.
POST /api/v1/clusters/{id}/san datastore.manage Provision pooled storage for the cluster. Changing the replication of a pool that already exists (a grow, "add disks") obeys the same refusal as protection/remediate: nothing runs while a member is in a fresh planned window or any group is short of copies, and the failed phase says why.
DELETE /api/v1/clusters/{id}/san datastore.manage Destroy it.

HCI status envelope

Everything below is additive: an older controller simply omits it, and clients must tolerate its absence. GET /api/v1/clusters/{id}/hci/status is served from the background-sampled cache, never a live storage call.

Field Meaning
stale RAW and fail-closed: the last sample did not answer, or is older than 20 s. Every safety gate reads this, never unreachable.
unreachable CONFIRMED outage: two consecutive failed sample ticks, or a sample older than 30 s. Only this turns the header to "unknown"; one failed tick is stale: true, unreachable: false.
pools_carried The datastore list is the last complete read (this sample's pool sub-read failed).
pools_unreadable Pool details could not be read for over two minutes and nothing is left to carry; show "could not read datastore details", never "no datastores yet".
datastores[] One user-facing datastore per kind: kind (block VM disks, files ISOs), label, usable_bytes, used_bytes, pools, protection (copies, min_copies, failure_domain, host_separated, proven, tolerated_host_failures, state: healthy | reduced | checking | unknown), compliance (compliant | below_recommended | reconciling | reduced | checking | unknown) and rechecking. checking means "cannot be proven right now" (placement groups re-peering, host layout not read yet): neutral, not a fault. A worse verdict is shown only after it is seen on three consecutive samples; rechecking: true marks a card that still shows the held verdict while a worse one is being confirmed.
rollup The header verdict: state (ok | maintenance | warn | err | unknown), headline, expected (health-check code to the reason it is expected), hosts_in_maintenance, margin (one_more_failure | none | unknown). unknown only for a confirmed outage; a single stale tick keeps the held state but the margin is unknown.
resilience "What if this host goes offline?" per storage host: as_of, stale, margin, hosts[] (host, in_maintenance, safe, newly_below, already_below, at_risk_pools, affected_disks, affected_vms (at most 10 names), is_mon, blockers) and separation (pgs_checked, violations, pools). safe is the enter-maintenance gate's own decision from a reading under 90 s old and complete; on any stale or partial reading stale is true, margin is unknown and no host is safe.
health_checks[].expected, .explained_by Set only when the WHOLE check is explained by hosts in planned maintenance (with the plain reason). A mixed picture keeps its checks raised. Planned maintenance never explains data-integrity codes or inaccessible data, a host in maintenance for over 24 hours (or whose planned restart did not finish cleanly) stops explaining anything, and a host in maintenance that is up but has only SOME of its disks down is a real failure, not the plan. The alarm engine still raises every check.
status.recovery.kind waiting (planned maintenance explains the missing copies and nothing is moving: see waiting_for), resync, rebuild or rebalance (only while data is really moving). waiting_for names the hosts whose return the cluster is waiting for. progress_pct is 1 − outstanding/peak since the last time nothing was outstanding.

Virtual Objects

GET /api/v1/clusters/{id}/hci/objects answers from a cache (collecting: true on the very first read, while the reading is taken in the background). It carries as_of, age_seconds, stale, source_host, objects_supported (false, with unsupported_hosts, when members run a SimpleVirt without the read), partial (sub-reads that failed), refresh_error, and:

  • owners {complete, reason}: an image is reported unattached only when every cluster host is reachable and every VM's state is known; otherwise its owner_state is unknown.
  • datastores[] {kind, label, pools, compliance, reason, copies, min_copies, failure_domain, host_separated, below_recommended}: host_separated is the LIVE fact "enough healthy hosts right now", not the pool's policy (failure_domain is).
  • objects[] {id, kind (disk|files), name, pool, image, size_bytes, owner {vm_uuid, vm_name, host_id, host_name, state}|null, owner_state (known|unattached|unknown), datastore, compliance (compliant|reconciling|reduced|inaccessible|unknown), reason, expected, expected_why}. An image is striped over about every placement group of its pool, so its compliance is its pool's. expected is true only while the whole picture is planned maintenance.
  • pools[], hosts[] {host, state (online|maintenance|down), kind, since, osds} and separation.

GET /api/v1/clusters/{id}/hci/objects/placement?pool=&image= returns one disk's sampled placement (hosts, sites, copies, min_copies, health, pool_compliance); 409 while the inventory is still being collected, 404 for a disk not in it. POST /api/v1/clusters/{id}/hci/retest returns {tested_at, took_ms, throttled?, objects_error?}.

HCI safety refusals and previews

A destructive storage change is refused, with a plain reason, while a member is in a fresh planned window (operator Maintenance Mode or an armed planned reboot/update): raising protection (protection/remediate, and any provisioning step that changes an existing pool's replication) answers 409; removing a disk or decommissioning a host answers 409 (and the preview shows a blocking Maintenance check). A member that no longer explains anything — in maintenance over 24 hours, silent longer than the anti-strand window, or whose restart did not finish cleanly — does not block; the previews list it as a warn Maintenance check instead, and the operation's own target host never blocks itself. Raising protection is also refused whenever the LIVE status shows a disk down, any group not active and clean, or no health reading, whatever an older placement reading says.

GET /api/v1/hosts/{id}/maintenance/preview gains, inside storage, source (snapshot | live | estimate; an estimate never says ok), label (plain provenance such as "as of 12s ago"), blockers (the gate's reasons), and affected_disks / affected_vms (what would stop serving I/O). The host's own gate now also refuses (overridable with force, like every other refusal) when the cluster's disk layout could not be read.


Preview feature endpoints

Each group below answers 403 with {"experimental": "<flag>"} unless its flag is enabled on the controller. Read preview features before integrating against any of them.

Kubernetes (preview: kubernetes)

Method Path Privilege Description
GET /api/v1/k8s vm.view Provisioned clusters.
GET /api/v1/k8s/{id} vm.view One cluster.
POST /api/v1/k8s vm.create Provision a cluster.
DELETE /api/v1/k8s/{id} vm.delete Delete a cluster and its nodes.
GET /api/v1/k8s/{id}/kubeconfig admin.users The cluster-admin kubeconfig.
GET /api/v1/k8s/{id}/joincommand admin.users The node bootstrap token and join command.

The last two hand back credentials, which is why they require admin.users and not vm.view — a read privilege that lets a viewer see a cluster must not also hand them its administrative key.

Stretched cluster

These endpoints need no experimental flag — the stretched cluster has graduated out of the preview gate. Safety is unchanged: enable runs a fail-closed readiness gate and refuses on any cluster that is not a genuine two-site topology with a witness. Full two-datacenter hardware validation is pending — see SIMPLEVIRT_STRETCHED_CLUSTER.md.

Method Path Privilege Description
GET /api/v1/clusters/{id}/stretch cluster.view The two-site topology and its lifecycle state.
PUT /api/v1/clusters/{id}/stretch cluster.ha Declare the topology: site membership and the witness.
DELETE /api/v1/clusters/{id}/stretch cluster.ha Clear the declaration.
POST /api/v1/clusters/{id}/stretch/preview cluster.view Dry-run a topology, read-only.
POST /api/v1/clusters/{id}/stretch/enable cluster.ha Activate, gated on readiness.
POST /api/v1/clusters/{id}/stretch/disable cluster.ha Deactivate.
GET /api/v1/clusters/{id}/stretch/maintenance/precheck cluster.view What taking a whole site offline would cost.

Workload distributor

Graduated out of preview — these routes need no flag. The distributor defaults to recommend-only, and every move it applies passes the same fail-closed migration precheck a manual live-migration does.

Method Path Privilege Description
POST /api/v1/clusters/{id}/drs cluster.manage Set the cluster's balancing mode and aggressiveness.
GET /api/v1/clusters/{id}/drs/recommendations cluster.view Proposed moves, each with its reason and expected effect.
POST /api/v1/clusters/{id}/drs/recommendations/{rid}/apply cluster.manage Apply one recommendation.
POST /api/v1/clusters/{id}/drs/rules cluster.manage Set a per-VM placement rule.
DELETE /api/v1/clusters/{id}/drs/rules/{vmuuid} cluster.manage Clear a per-VM rule.

File services (preview: file_services)

Method Path Privilege Description
GET /api/v1/clusters/{id}/file-services cluster.view The File Services view: policy, declared shares, and an honest readiness signal.
POST /api/v1/clusters/{id}/file-services cluster.manage Enable/disable and set the floating address and protocols.
POST /api/v1/clusters/{id}/file-services/shares cluster.manage Declare a share (name, protocol, quota, read-only, allowed clients, SMB valid users).
PUT /api/v1/clusters/{id}/file-services/shares/{sid} cluster.manage Edit a share in place (quota, read-only, allowed clients, SMB valid users); keeps its id and data.
DELETE /api/v1/clusters/{id}/file-services/shares/{sid} cluster.manage Remove a declared share. Data is retained by default; ?delete_data=true also reclaims it.
POST /api/v1/clusters/{id}/file-services/smb-accounts cluster.manage Mint a local SMB service account; the password is returned once.
POST /api/v1/clusters/{id}/file-services/smb-accounts/{user}/rotate cluster.manage Rotate an SMB account's password (returned once).
DELETE /api/v1/clusters/{id}/file-services/smb-accounts/{user} cluster.manage Remove an SMB service account.

Cluster services (preview: cluster_services)

Method Path Privilege Description
GET /api/v1/clusters/{id}/cluster-services cluster.view The cluster-services view: membership, authority and per-host state. Returns an enabled: false shell when nothing has been sampled or no member runs the service, so a client shows "not present" rather than an error.

Block datastores (preview: block_datastores)

This preview gates a type, not a path. POST /api/v1/hosts/{id}/datastores refuses an iSCSI or Fibre Channel datastore unless the flag is on; every other datastore type is unaffected.


The host agent API

Each SimpleVirt host runs simplevirtd, which serves its own HTTP API on three surfaces. None of them is a client integration surface. They are documented here so that a reader who finds them on the wire knows what they are and why not to call them.

Surface Authentication Routes What it is
/api/v1/agent/... Authorization: Bearer <agent token> 117 The controller-to-host channel. SimpleVirt-Command holds a per-host token installed at pairing and calls these to read inventory and drive the host. The shapes are negotiated between the two builds at either end and change without notice.
/api/v1/... Host session cookie + CSRF 85 The host's own recovery console — the local web UI an operator uses when the controller is unavailable. It mirrors the agent surface with a session instead of a token.
/api/v1/cluster/... Loopback only + a cluster-control token 10 The cluster-services control surface. Refused outright from any non-loopback address.
/healthz, GET /api/v1/version, POST /api/v1/auth/login, /, /api/ none 5 Liveness, build string, host sign-in, the host's embedded console, and the API 404 catch-all.

Everything a client legitimately needs from a host is already proxied by the controller under /api/v1/hosts/{id}/..., with the controller's privilege model applied and the action recorded in the audit log. Calling a host directly bypasses both. Use the controller.


Coverage of this document

Every route registered by SimpleVirt-Command appears in exactly one section above: 216 paths under /api/v1, plus /healthz and the console catch-all — 218 registrations in total. There is no separate flat index, deliberately: a second copy of the route table is a second thing to keep true.

Two things are stated once rather than per endpoint, and apply everywhere:

  1. Authentication — session cookie on everything, X-CSRF-Token on every mutating request. See authentication.
  2. Errors — {"error": "…"} with a meaningful status code. See conventions.

Where an endpoint's request or response shape is not obvious from its name, it is described in its section. Where it is obvious — a list endpoint returning the objects it lists — it is not padded out.

This page is part of the SimpleVirt 1.0.0 documentation set. The same guides ship inside the appliance images at /usr/share/doc/simplevirt.