Live Disk Format Migration
Convert a running VM’s disk image to a different format — most commonly raw → qcow2 — with zero downtime. The VM continues serving workloads throughout the entire operation.
This is powered by QEMU’s block-mirror job: pivirtd starts a background mirror to a new target file, polls until data transfer reaches 100%, then atomically pivots the guest’s I/O to the new image.
When to Use This
Section titled “When to Use This”raw images are fast but consume their full allocated size immediately and offer no snapshot support. Converting to qcow2 gives you:
- Thin provisioning — only used blocks consume disk space (often 30–60% savings)
- Snapshot support — internal QEMU snapshots
- zstd compression — enabled by default during the mirror
- Native copy-on-write
Convert back to raw when you need maximum I/O predictability (no CoW overhead), compatibility with external tools that cannot read qcow2, or when the disk is nearly full and thin provisioning provides no benefit.
Prerequisites
Section titled “Prerequisites”pivirtctlinstalled and configured (socket at$PIVIRTD_SOCKETor/var/lib/pivirt/daemon/pivirtd.sock)- The VM must be running — the live mirror requires an active QEMU process
- Sufficient disk space for the target image alongside the source
Single VM Migration
Section titled “Single VM Migration”-
Check the current disk format
Terminal window pivirtctl vm info my-vmLook for the
Storagesection:┌ Storage ──────────────────────────────────────────────────────────Disk 1 10 GB raw on virtio (/var/lib/pivirt/vms/my-vm/disks/system.raw) -
Start the live disk move
Terminal window pivirtctl vm disk-move my-vm --format qcow2The daemon starts a background mirror job. The command returns immediately once the job is accepted, printing the job ID and the source path that will be kept:
TYPE DEVICE STATUS PROGRESS TRANSFERRED TOTAL TARGETdisk-move drive0 active 0.0% 0 B 10 GB /var/lib/pivirt/vms/my-vm/disks/system.qcow2Source kept at: /var/lib/pivirt/vms/my-vm/disks/system.raw -
Monitor progress
Terminal window # Poll every 5 seconds until completepivirtctl vm disk-job my-vm --watch 5Or check the VM’s full status (includes blocker task and ETA):
Terminal window pivirtctl vm status my-vm┌ Phase ─────────────────────────────────────────────────────────────Phase RunningBlocker Task disk-move in progress (drive0 → system.qcow2, 47%, ETA unknown)┌ Jobs ──────────────────────────────────────────────────────────────disk-move device=drive0 status=active progress=47.0% transferred=4.7 GB total=10.0 GB ... -
Confirm completion
When the job status reaches
completed, the QEMU process has already pivoted all I/O to the new image. No manual finalization is needed.┌ Events ─────────────────────────────────────────────────────────────Normal DiskMoveStarted: system.raw → system.qcow2Normal DiskMoveCompleted: system.raw → system.qcow2 (format qcow2) -
Restart to apply the new disk path in the VM config
A restart is required to flush QEMU’s in-memory block graph and pick up the new disk path in
vm info:Terminal window pivirtctl vm stop my-vmpivirtctl vm start my-vmAfter restart,
vm inforeflects the new format:┌ Storage ──────────────────────────────────────────────────────────Disk 1 10 GB qcow2 on virtio (/var/lib/pivirt/vms/my-vm/disks/system.qcow2) -
Remove the source image
Once the VM is verified healthy, reclaim the space:
Terminal window sudo rm /var/lib/pivirt/vms/my-vm/disks/system.raw
Batch Migration
Section titled “Batch Migration”Migrate all VMs with raw disks in parallel. Because the mirror runs inside QEMU, multiple jobs compete for disk bandwidth — consider spreading them if I/O matters.
-
Find all VMs with raw disks
Terminal window pivirtctl vm list -o json | python3 -c "import json, sysfor vm in json.load(sys.stdin):for disk in vm.get('disks', []):if disk.get('format') == 'raw':print(vm['name'], disk['path'])" -
Start all jobs in parallel
Terminal window for vm in vm-a vm-b vm-c; dopivirtctl vm disk-move "$vm" --format qcow2 &donewait -
Watch all jobs until complete
Terminal window while true; doall_done=truefor vm in vm-a vm-b vm-c; dostatus=$(pivirtctl vm disk-job "$vm" 2>&1 | awk 'NR==2 {print $3, $4}')echo "$vm: $status"echo "$status" | grep -q "active\|ready" && all_done=falsedone$all_done && breaksleep 10done -
Restart each VM sequentially
Terminal window for vm in vm-a vm-b vm-c; dopivirtctl vm stop "$vm"sleep 2pivirtctl vm start "$vm"done
Advanced Options
Section titled “Advanced Options”Bandwidth Throttling
Section titled “Bandwidth Throttling”Limit the mirror’s I/O impact on the running workload:
# Limit to 100 MB/spivirtctl vm disk-move my-vm --format qcow2 --speed 104857600Disable Compression
Section titled “Disable Compression”By default, zstd compression is applied during the copy (reduces disk writes for sparse/compressible data). Disable it if the source is already compressed or CPU is the bottleneck:
pivirtctl vm disk-move my-vm --format qcow2 --no-compressionCustom Target Path
Section titled “Custom Target Path”Override the daemon-computed target path:
pivirtctl vm disk-move my-vm \ --format qcow2 \ --target /data/fast-pool/my-vm/system.qcow2Incremental Sync with Bitmap
Section titled “Incremental Sync with Bitmap”For subsequent moves (e.g. migrating to a different storage pool), use a dirty bitmap to copy only changed blocks:
# First: create bitmap during initial movepivirtctl vm disk-move my-vm --format qcow2 --bitmap my-bitmap
# Later: sync only dirty blockspivirtctl vm disk-move my-vm \ --format qcow2 \ --sync bitmap \ --bitmap my-bitmapError Policies
Section titled “Error Policies”Control what happens when a read or write error occurs mid-mirror:
pivirtctl vm disk-move my-vm \ --format qcow2 \ --on-source-error report \ # report (default), enospc --on-target-error reportCancelling a Job
Section titled “Cancelling a Job”pivirtctl vm disk-job my-vm --cancelMonitoring
Section titled “Monitoring”Job Status Values
Section titled “Job Status Values”| Status | Meaning |
|---|---|
active |
Mirror is copying data |
ready |
Mirror is in sync; pivot is in progress |
completed |
Pivot done; guest is on the new disk |
failed |
Mirror failed; source disk still active |
cancelled |
Job was cancelled; source disk still active |
VM Status Phases
Section titled “VM Status Phases”While a disk-move job is running, vm status reports a Blocker Task:
pivirtctl vm status my-vm┌ Phase ─────────────────────────────────────────────────────────Phase RunningWorking yesBlocker Task disk-move in progress (drive0 → system.qcow2, 72%, ETA unknown)Streaming Events
Section titled “Streaming Events”pivirtctl vm events my-vmKey events emitted during a disk move:
| Event | Meaning |
|---|---|
DiskMoveStarted |
Mirror job accepted by QEMU |
DiskMoveCompleted |
Pivot successful; guest on new disk |
DiskMoveFailed |
Mirror job failed |
Related
Section titled “Related”pivirtctl vm disk-move— command referencepivirtctl vm disk-job— monitoring and cancel- Live Migration — cross-node VM migration
- Storage Concepts