Skip to content

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.

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
  • pivirtctl installed and configured (socket at $PIVIRTD_SOCKET or /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
  1. Check the current disk format

    Terminal window
    pivirtctl vm info my-vm

    Look for the Storage section:

    ┌ Storage ──────────────────────────────────────────────────────────
    Disk 1 10 GB raw on virtio (/var/lib/pivirt/vms/my-vm/disks/system.raw)
  2. Start the live disk move

    Terminal window
    pivirtctl vm disk-move my-vm --format qcow2

    The 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 TARGET
    disk-move drive0 active 0.0% 0 B 10 GB /var/lib/pivirt/vms/my-vm/disks/system.qcow2
    Source kept at: /var/lib/pivirt/vms/my-vm/disks/system.raw
  3. Monitor progress

    Terminal window
    # Poll every 5 seconds until complete
    pivirtctl vm disk-job my-vm --watch 5

    Or check the VM’s full status (includes blocker task and ETA):

    Terminal window
    pivirtctl vm status my-vm
    ┌ Phase ─────────────────────────────────────────────────────────────
    Phase Running
    Blocker 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 ...
  4. 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.qcow2
    Normal DiskMoveCompleted: system.raw → system.qcow2 (format qcow2)
  5. 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-vm
    pivirtctl vm start my-vm

    After restart, vm info reflects the new format:

    ┌ Storage ──────────────────────────────────────────────────────────
    Disk 1 10 GB qcow2 on virtio (/var/lib/pivirt/vms/my-vm/disks/system.qcow2)
  6. 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

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.

  1. Find all VMs with raw disks

    Terminal window
    pivirtctl vm list -o json | python3 -c "
    import json, sys
    for vm in json.load(sys.stdin):
    for disk in vm.get('disks', []):
    if disk.get('format') == 'raw':
    print(vm['name'], disk['path'])
    "
  2. Start all jobs in parallel

    Terminal window
    for vm in vm-a vm-b vm-c; do
    pivirtctl vm disk-move "$vm" --format qcow2 &
    done
    wait
  3. Watch all jobs until complete

    Terminal window
    while true; do
    all_done=true
    for vm in vm-a vm-b vm-c; do
    status=$(pivirtctl vm disk-job "$vm" 2>&1 | awk 'NR==2 {print $3, $4}')
    echo "$vm: $status"
    echo "$status" | grep -q "active\|ready" && all_done=false
    done
    $all_done && break
    sleep 10
    done
  4. Restart each VM sequentially

    Terminal window
    for vm in vm-a vm-b vm-c; do
    pivirtctl vm stop "$vm"
    sleep 2
    pivirtctl vm start "$vm"
    done

Limit the mirror’s I/O impact on the running workload:

Terminal window
# Limit to 100 MB/s
pivirtctl vm disk-move my-vm --format qcow2 --speed 104857600

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:

Terminal window
pivirtctl vm disk-move my-vm --format qcow2 --no-compression

Override the daemon-computed target path:

Terminal window
pivirtctl vm disk-move my-vm \
--format qcow2 \
--target /data/fast-pool/my-vm/system.qcow2

For subsequent moves (e.g. migrating to a different storage pool), use a dirty bitmap to copy only changed blocks:

Terminal window
# First: create bitmap during initial move
pivirtctl vm disk-move my-vm --format qcow2 --bitmap my-bitmap
# Later: sync only dirty blocks
pivirtctl vm disk-move my-vm \
--format qcow2 \
--sync bitmap \
--bitmap my-bitmap

Control what happens when a read or write error occurs mid-mirror:

Terminal window
pivirtctl vm disk-move my-vm \
--format qcow2 \
--on-source-error report \ # report (default), enospc
--on-target-error report
Terminal window
pivirtctl vm disk-job my-vm --cancel
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

While a disk-move job is running, vm status reports a Blocker Task:

Terminal window
pivirtctl vm status my-vm
┌ Phase ─────────────────────────────────────────────────────────
Phase Running
Working yes
Blocker Task disk-move in progress (drive0 → system.qcow2, 72%, ETA unknown)
Terminal window
pivirtctl vm events my-vm

Key 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