Skip to content

VM Hardware Editor — 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.

  • 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: true and apply_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: false and apply_mode: pending_reboot with a warnings[] 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.

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)
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
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
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.

  • 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_edits returns the after-state the dashboard shows in the diff drawer; compute_diff returns the per-field changes.
  • Frontend mirror: crates/web-ux-v2/src/stores/vmHardware.ts (via the daemon’s preview endpoint).

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:

  1. A new max_vcpus field in the VM context, persisted at create time (operator picks “headroom” up-front; non-editable while running).
  2. The renderer must emit <vcpu current='N' placement='static'>M</vcpu> with M > N and a topology that multiplies to M.
  3. 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.