VM Hardware Editor — Hot-plug matrix
Hot-plug matrix
Section titled “Hot-plug matrix”Source of truth for how hardware edits flow against a running VM. The
SynVirt rule, applied in every handler under api/vm_edit/, is:
Try live, fall back to next-boot, never block.
The editor never refuses an edit because the VM is running. If QEMU
or the guest cannot hot-apply the change, the handler writes the
change to the persistent XML (virsh define, config-only) and
reports it as PendingReboot with a clear warning. The running VM
keeps serving its current shape until the operator restarts it.
The only edits that fail are the ones that are mathematically or semantically impossible (topology that doesn’t multiply to the vCPU count, Secure Boot on BIOS firmware, shrinking a disk, etc.) — those are validation errors, not VM-state errors.
Apply modes
Section titled “Apply modes”- Live — applied to both the running QEMU instance and the
persistent XML. The operator sees the change immediately inside
the guest. The handler reports
applied_live: trueandapply_mode: live. - PendingReboot — written to the persistent XML; takes effect
on the next guest start. The current QEMU keeps the old value.
Reported as
applied_live: falseandapply_mode: pending_rebootwith awarnings[]entry explaining why live failed (or the generic"VM is running — applied to next-start config").
RequiresShutoff is no longer an outcome of the apply path —
the handler will never refuse on VM state. The token still appears
in the preview summary for fields that can be classified
statically as needing a restart on the running VM (so the dashboard
can warn ahead of time), but the apply itself degrades to
PendingReboot.
Table — single-field PATCH endpoints
Section titled “Table — single-field PATCH endpoints”| Field | Live-attemptable on running VM? | When live fails | Notes |
|---|---|---|---|
cpu.vcpus (alone) |
yes (setvcpus --live --config) |
falls back to --config with "vCPU count change could not be hot-applied (no headroom declared on the domain); takes effect on next start" |
Headroom requires <vcpu current=…>N</vcpu> — see Follow-up: CPU hot-add headroom model |
cpu.vcpus (with topology) |
no | PendingReboot — single atomic define_xml carries vcpus + topology together so libvirt’s product check passes |
libvirt requires sockets×cores×threads == max-vcpus at define time |
cpu.sockets / cores / threads |
no | PendingReboot | Rewrites <topology> |
cpu.mode |
no | PendingReboot | Rewrites <cpu mode=…> |
memory.current_mib (balloon target) |
yes (setmem --live) |
PendingReboot with "balloon adjust not accepted live" |
Live call succeeds even when the guest has no virtio_balloon driver — in that case the daemon emits "Live balloon target sent; requires the guest virtio_balloon driver to actually shrink" so the operator can verify with virsh dommemstat |
memory.max_mib (raise) |
no | PendingReboot with "maximum memory cannot be raised live" |
Lowering also requires next-boot |
memory.hugepages / locked / balloon_model |
no | PendingReboot | Backing read at QEMU spawn |
firmware.kind (BIOS ↔ UEFI) |
no | PendingReboot | NVRAM template baked at first define of UEFI form. Round-trip UEFI→BIOS is supported — the handler normalises <os firmware='efi'> back to <os> and strips <firmware> / <loader> / <nvram> lines |
firmware.secure_boot (true) on BIOS |
rejected (E_FIRMWARE_BIOS_NO_SECURE_BOOT, HTTP 400) |
n/a | Semantically invalid — must switch firmware to UEFI in the same edit |
firmware.secure_boot (on UEFI) |
no | PendingReboot | Toggles SMM + OVMF code path |
firmware.nvram_template |
no | PendingReboot | Template is consumed at first start |
chipset.family (q35 ↔ i440fx) |
no | PendingReboot with destructive warning | PCI root layout swap; guest drivers may need re-install |
chipset.machine (version bump) |
no | PendingReboot | Machine type fixed at spawn |
boot.order |
no | PendingReboot | Persistent XML only |
boot.menu_enabled / menu_timeout_ms |
no | PendingReboot | Same |
display.video / vram_mib / heads / accel3d |
no | PendingReboot with "VM is running — applied to next-start config" |
<video> device attaches at spawn |
tpm.enabled (add/remove) |
no | PendingReboot | swtpm lifecycle bound to QEMU |
tpm.version / tpm.model / tpm.backend |
no | PendingReboot | Same |
| Action | Live-attemptable on running VM (virtio)? | When live fails | Notes |
|---|---|---|---|
| Attach disk | yes (attach-device --live --config) |
PendingReboot via --config |
virtio supports hot-plug; SATA/IDE typically falls back |
| Detach disk | yes | PendingReboot via --config |
Same |
PUT /disks/:target driver knobs (cache / discard / io / serial / boot_order) |
no — <driver> attrs read at spawn |
PendingReboot | The handler rewrites the persistent XML and defines it. boot_order migrates legacy <boot dev=> in <os> to per-device <boot order=> automatically so the resulting XML is consistent (no mixed model that libvirt would reject) |
PUT /disks/:target io='native' without compatible cache |
rejected (E_INVALID HTTP 400) |
n/a | Preflight validates: io=native requires cache=none|directsync |
Resize grow (blockresize) |
yes (only with VM running) | n/a — fails offline with "domain is not running" |
virsh requires running |
| Resize shrink | always rejected | n/a | libvirt forbids |
DELETE /disks/:target?delete_backing=true |
yes | n/a | After successful detach, the daemon removes the backing file via tokio::fs::remove_file. Volume backings are NOT auto-deleted (storage-pool cleanup not yet wired) — the handler logs and skips. Block device backings are never deleted (they belong to the host) |
Network adapters
Section titled “Network adapters”| Action | Live-attemptable on running VM? | When live fails | Notes |
|---|---|---|---|
| Attach NIC | yes (attach-device --live --config) |
falls back to --config |
OVS sometimes rejects the runtime port-add (Operation not supported); the daemon catches that and degrades to next-start, reporting applied_live: false |
| Detach NIC | yes | PendingReboot via --config |
Same fallback path |
PUT /nics/:mac virtswitch / virtlan_tag |
yes | PendingReboot | update-device --live |
| Edit model (virtio ↔ e1000e ↔ rtl8139) | no | PendingReboot | Device class swap on PCI bus |
Controllers
Section titled “Controllers”| Action | Live-attemptable on running VM? | When live fails | Notes |
|---|---|---|---|
| Add USB / SCSI / SATA / virtio-serial | no | PendingReboot | PCI bus enumeration at spawn. Adding a SCSI disk auto-creates an lsilogic controller if no <controller type='scsi'> exists — explicit POST /controllers with model=virtio-scsi is needed for the modern bus |
| Remove controller | no | PendingReboot | Same |
| PCI root controllers | not editable | n/a | libvirt-managed |
Host PCI passthrough
Section titled “Host PCI passthrough”| Action | Live-attemptable on running VM? | When live fails | Notes |
|---|---|---|---|
| Assign device | no | PendingReboot | VFIO unbind + IOMMU isolation at spawn |
| Unassign device | no | PendingReboot | Same |
| List IOMMU groups | always (read-only) | n/a | Reads /sys/kernel/iommu_groups/ |
Misc devices (read-only after VM creation)
Section titled “Misc devices (read-only after VM creation)”Input / sound / serial / RNG / watchdog devices are provisioned at VM creation through the profile catalog. The editor does not currently expose add/remove endpoints for these classes.
Implementation pointers
Section titled “Implementation pointers”- Backend handlers:
crates/daemon/src/api/vm_edit/patch.rs(CPU, memory, firmware, chipset, boot, display, TPM),disks.rs(disk CRUD + driver knobs),nics.rs(NIC CRUD),controllers.rs,hostpci.rs. - Preview / projection:
crates/daemon/src/api/vm_edit/apply.rs.project_editsreturns the after-state the dashboard shows in the diff drawer;compute_diffreturns the per-field changes. - Frontend mirror:
crates/web-ux-v2/src/stores/vmHardware.ts(via the daemon’s preview endpoint).
Follow-up: CPU hot-add headroom model
Section titled “Follow-up: CPU hot-add headroom model”The renderer (crates/daemon/src/libvirt_renderer/elements/cpu.rs)
emits <vcpu placement='static'>N</vcpu> — a single number that
serves as both current and maximum. Libvirt’s setvcpus --live
refuses any value greater than max, so cpu.vcpus increases on a
running VM always fall back to PendingReboot today.
To enable real hot-add when the guest OS supports it (modern Linux, Server editions of Windows), the model needs:
- A new
max_vcpusfield in the VM context, persisted at create time (operator picks “headroom” up-front; non-editable while running). - The renderer must emit
<vcpu current='N' placement='static'>M</vcpu>withM > Nand a topology that multiplies toM. - The wizard (
web-ux-v2) needs a “VM max CPUs” field on the CPU step.
This is a feature, not a bug. The current behaviour (“always cap to PendingReboot”) is honest given the renderer model.
If a row in this table disagrees with what the editor displays in its Banner, the table wins — the backend handlers and this doc are the source of truth.