| your Linux construction kit
Source

KVM Virtual Machines on kldload

kldload systems make excellent KVM hypervisors — ZFS gives you instant snapshots, CoW cloning, and compression for VM disk images.

Works the same on CentOS/RHEL and Debian.


Install KVM

CentOS/RHEL

kpkg install qemu-kvm libvirt virt-install libguestfs-tools
systemctl enable --now libvirtd

Debian

kpkg install qemu-kvm libvirt-daemon-system virtinst libguestfs-tools
systemctl enable --now libvirtd

Verify

virsh list --all
virt-host-validate

Set up ZFS storage for VMs

# Create a dataset for VM images
zfs create -o mountpoint=/var/lib/libvirt/images \
           -o compression=lz4 \
           -o recordsize=64k \
           rpool/vms

recordsize=64k is a good balance for VM disk I/O (mix of small and large operations).


Create a VM from the kldload ISO

ISO=$(ls -t ~/kldload-free/live-build/output/*.iso 2>/dev/null | head -1)

virt-install \
  --name my-server \
  --ram 4096 \
  --vcpus 4 \
  --disk path=/var/lib/libvirt/images/my-server.qcow2,size=40,format=qcow2 \
  --cdrom "$ISO" \
  --os-variant centos-stream9 \
  --network bridge=br0 \
  --graphics vnc,listen=0.0.0.0 \
  --boot uefi \
  --noautoconsole

Connect via VNC to complete the install:

virsh vncdisplay my-server
# :0  → connect to host:5900

Create a VM from a cloud image

CentOS Stream 9

curl -LO https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2
cp CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2 /var/lib/libvirt/images/centos-cloud.qcow2

# Resize the disk
qemu-img resize /var/lib/libvirt/images/centos-cloud.qcow2 40G

# Set a root password with virt-customize
virt-customize -a /var/lib/libvirt/images/centos-cloud.qcow2 \
  --root-password password:changeme \
  --hostname my-centos-vm

virt-install \
  --name centos-cloud \
  --ram 2048 --vcpus 2 \
  --disk /var/lib/libvirt/images/centos-cloud.qcow2 \
  --os-variant centos-stream9 \
  --network bridge=br0 \
  --boot uefi \
  --import --noautoconsole

Debian

curl -LO https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-generic-amd64-daily.qcow2
cp debian-13-generic-amd64-daily.qcow2 /var/lib/libvirt/images/debian-cloud.qcow2
qemu-img resize /var/lib/libvirt/images/debian-cloud.qcow2 40G

virt-customize -a /var/lib/libvirt/images/debian-cloud.qcow2 \
  --root-password password:changeme \
  --hostname my-debian-vm

virt-install \
  --name debian-cloud \
  --ram 2048 --vcpus 2 \
  --disk /var/lib/libvirt/images/debian-cloud.qcow2 \
  --os-variant debian12 \
  --network bridge=br0 \
  --boot uefi \
  --import --noautoconsole

Golden images and CoW cloning

Create one base image, then clone it instantly for each new VM:

Build the golden image

# Install a VM, configure it how you want, then shut it down
virsh shutdown my-server

# Generalize it (remove machine-specific state)
virt-sysprep -d my-server \
  --operations defaults,-ssh-userdir \
  --hostname localhost

# Mark it as a template (prevent accidental boot)
virsh dominfo my-server

Clone with qemu-img (fastest)

# CoW clone — near-instant, near-zero space
qemu-img create -f qcow2 \
  -b /var/lib/libvirt/images/my-server.qcow2 \
  -F qcow2 \
  /var/lib/libvirt/images/web-1.qcow2

# Register the clone as a new VM
virt-install \
  --name web-1 \
  --ram 2048 --vcpus 2 \
  --disk /var/lib/libvirt/images/web-1.qcow2 \
  --os-variant centos-stream9 \
  --network bridge=br0 \
  --boot uefi \
  --import --noautoconsole

Clone with virt-clone (copies everything)

# Full copy (slower but independent — no backing file dependency)
virt-clone \
  --original my-server \
  --name web-2 \
  --auto-clone

Customize after cloning

# Set unique hostname on the clone
virt-customize -d web-1 --hostname web-1.infra.local

# Or SSH in after boot:
virsh start web-1
ssh root@<ip> hostnamectl set-hostname web-1.infra.local

Snapshots

Faster and more reliable than libvirt snapshots:

# Snapshot a VM's disk
zfs snapshot rpool/vms@web-1-before-update

# Roll back
virsh shutdown web-1
zfs rollback rpool/vms@web-1-before-update
virsh start web-1

Libvirt snapshots

# Create
virsh snapshot-create-as web-1 --name before-update --description "pre-update snapshot"

# List
virsh snapshot-list web-1

# Revert
virsh snapshot-revert web-1 before-update

# Delete
virsh snapshot-delete web-1 before-update

VM lifecycle

# Start / stop / restart
virsh start web-1
virsh shutdown web-1       # graceful
virsh destroy web-1        # force stop (like pulling the power cord)
virsh reboot web-1

# Pause / resume
virsh suspend web-1
virsh resume web-1

# Auto-start on host boot
virsh autostart web-1
virsh autostart --disable web-1

# Delete VM and its disk
virsh undefine web-1 --nvram --remove-all-storage

# Console access
virsh console web-1        # serial console (Ctrl+] to exit)

Resource management

# Change RAM (requires shutdown)
virsh setmaxmem web-1 8G --config
virsh setmem web-1 8G --config

# Change vCPUs (requires shutdown)
virsh setvcpus web-1 4 --config --maximum
virsh setvcpus web-1 4 --config

# Hot-add a disk
qemu-img create -f qcow2 /var/lib/libvirt/images/web-1-data.qcow2 100G
virsh attach-disk web-1 /var/lib/libvirt/images/web-1-data.qcow2 vdb \
  --driver qemu --subdriver qcow2 --persistent

# Hot-add a network interface
virsh attach-interface web-1 bridge br0 --model virtio --persistent

Monitoring

# List all VMs with state
virsh list --all

# CPU/memory stats
virt-top

# Disk I/O stats
virsh domblkstat web-1 vda

# Network stats
virsh domifstat web-1 vnet0

# Get IP address of a VM (requires qemu-guest-agent)
virsh domifaddr web-1

Migrate VMs between hosts

# Live migration (VM stays running)
virsh migrate --live web-1 qemu+ssh://other-host/system

# Offline migration (VM is shut down)
virsh shutdown web-1
# Copy the disk over ZFS send/receive:
zfs snapshot rpool/vms@migrate
zfs send rpool/vms@migrate | ssh other-host zfs receive tank/vms
# Then define the VM on the other host

Troubleshooting

# VM won't start — check logs
virsh start web-1 2>&1
journalctl -u libvirtd --since "5 minutes ago"
cat /var/log/libvirt/qemu/web-1.log

# Permission denied on disk
ls -la /var/lib/libvirt/images/web-1.qcow2
# Should be owned by qemu:qemu (CentOS) or libvirt-qemu:kvm (Debian)
chown qemu:qemu /var/lib/libvirt/images/web-1.qcow2  # CentOS
chown libvirt-qemu:kvm /var/lib/libvirt/images/web-1.qcow2  # Debian

# SELinux blocking (CentOS only)
ausearch -m avc --ts recent
restorecon -Rv /var/lib/libvirt/images/