libvirt and KVM
Run KVM guests with virsh and virt-install: cloud-image builds with cloud-init, snapshots, storage, NAT and bridged networking, PCI passthrough, live migration and the usual failures.
On this page
Cheatsheet#
| Task | Command |
|---|---|
| Talk to the system daemon, not the user session | export LIBVIRT_DEFAULT_URI=qemu:///system |
| List every domain, running or not | virsh list --all |
| Start, shut down cleanly, pull the plug | virsh start my-vm, virsh shutdown my-vm, virsh destroy my-vm |
Serial console (exit with Ctrl-]) | virsh console my-vm |
| Guest IP addresses | virsh domifaddr my-vm --source agent |
| Which disks a guest uses | virsh domblklist my-vm |
Edit the XML in $EDITOR and validate | virsh edit my-vm |
| Create from a cloud image, no ISO install | virt-install --import --disk my-vm.qcow2 --cloud-init user-data=user.yaml ... |
| Disk-only snapshot of a running guest | virsh snapshot-create-as my-vm pre-upgrade --disk-only --atomic --quiesce |
| Merge a snapshot overlay back into the base | virsh blockcommit my-vm vda --active --pivot |
| Grow a qcow2 image by 20 GiB (guest offline) | qemu-img resize my-vm.qcow2 +20G |
| Grow a disk of a running guest | virsh blockresize my-vm vda 60G |
| Convert and compact an image | qemu-img convert -p -O qcow2 -c in.qcow2 out.qcow2 |
| Storage pools and their volumes | virsh pool-list --all, virsh vol-list default |
| Networks and DHCP leases | virsh net-list --all, virsh net-dhcp-leases default |
| Hot-add a disk, persist it | virsh attach-disk my-vm /var/lib/libvirt/images/data.qcow2 vdb --subdriver qcow2 --persistent |
| Change memory and vCPUs at next boot | virsh setmaxmem my-vm 8G --config; virsh setvcpus my-vm 4 --config --maximum |
| Autostart with the host | virsh autostart my-vm |
| Live migrate to another host | virsh migrate --live --persistent --undefinesource my-vm qemu+ssh://kvm2.example.com/system |
| Remove a guest and its disks | virsh undefine my-vm --remove-all-storage --nvram |
| Check the host can run KVM | virt-host-validate qemu |
Commands assume libvirt 10.x or later, virt-install 4.x or later and QEMU 8.x or later on Fedora or RHEL 9. Reference: libvirt documentation, virt-install(1).
The model#
KVM is the kernel module that lets QEMU run a guest’s code on the CPU at near-native speed. QEMU is the process that emulates the machine: disks, NICs, display, firmware. libvirt is the daemon and API that manages those QEMU processes from XML definitions, and virsh is its shell. Everything you do in virsh becomes an XML document the daemon stores under /etc/libvirt/qemu/ and a QEMU command line it generates from that XML.
Two daemons matter. qemu:///system is the privileged daemon under /var/lib/libvirt/; it owns bridges, can pass through devices and is what production hosts use. qemu:///session runs as your user under ~/.local/share/libvirt/, cannot create bridges and does user-mode (SLIRP or passt) networking only. virsh picks the session URI when run as an unprivileged user unless LIBVIRT_DEFAULT_URI says otherwise, which is why a VM created in virt-manager can be invisible from a terminal.
virsh uri # which daemon this shell is talking to
export LIBVIRT_DEFAULT_URI=qemu:///system # set in ~/.bashrc on hosts you manage
usermod -aG libvirt "$USER" # polkit lets libvirt group members use the system daemon without sudo
virsh -c qemu+ssh://kvm2.example.com/system list # remote daemon over SSH; needs virsh on the far sideSince libvirt 9 Fedora and RHEL run modular daemons (virtqemud, virtnetworkd, virtstoraged) socket-activated by systemd rather than the monolithic libvirtd. systemctl status virtqemud.socket is the unit to check when virsh cannot connect.
| Term | Meaning |
|---|---|
| Domain | A guest VM: its XML definition plus, when running, a QEMU process |
| Persistent vs transient | Defined with virsh define and survives shutdown, or created with virsh create and vanishes at power-off |
| Storage pool | A directory, LVM VG, iSCSI target, NFS export or Ceph pool libvirt allocates volumes from |
| Volume | A disk image or logical volume inside a pool |
| Network | A virtual switch libvirt manages: a Linux bridge with optional dnsmasq for DHCP and DNS |
| Node device | A host PCI, USB or SCSI device that can be detached from the host and given to a guest |
| Snapshot | Either internal (inside the qcow2) or external (a new overlay file whose backing file is the old disk) |
Domains with virsh#
virsh list --all --title # includes the <title> field; useful when names are opaque
virsh dominfo my-vm # state, vCPUs, memory, autostart, persistence
virsh domstate my-vm --reason # "shut off (destroyed)" versus "shut off (shutdown)" tells you how it stopped
virsh domstats my-vm --cpu-total --balloon --block # live counters without a guest agent
virsh dumpxml my-vm > my-vm.xml # live XML as the daemon sees it, after defaults are applied
virsh dumpxml my-vm --inactive # the persistent definition used at next boot
virsh shutdown my-vm # ACPI power button; the guest decides whether to honour it
virsh shutdown my-vm --mode agent # ask the qemu-guest-agent instead; works when the guest ignores ACPI
virsh reboot my-vm
virsh destroy my-vm # SIGKILL to QEMU: filesystems are not flushed
virsh destroy my-vm --graceful # try to flush disk caches first, then kill
virsh suspend my-vm; virsh resume my-vm # pause the vCPUs; memory stays allocated
virsh managedsave my-vm # write memory to disk and stop; the next `virsh start` restores it
virsh managedsave-remove my-vm # discard that saved state so the next start is a cold boot
virsh setvcpus my-vm 4 --live # hot-plug up to the configured maximum
virsh setmem my-vm 4G --live # balloon down or up, within <maxMemory>
virsh autostart my-vm --disable
virsh domrename my-vm my-vm-old # inactive domains onlyvirsh shutdown returns immediately and the guest may take a minute or ignore it entirely (a guest with no ACPI daemon, or a Windows guest at the login screen). Poll virsh domstate before assuming it stopped, and reach for destroy only when data loss inside the guest is acceptable or the filesystem is journalled and idle.
Building a guest from a cloud image#
Cloud images (Fedora Cloud, RHEL KVM guest image, Ubuntu cloud, Debian genericcloud) boot in seconds, expect cloud-init to set the user and SSH key, and are the right base for any Linux guest. virt-install --import skips the installer, and --cloud-init writes a NoCloud seed ISO that is attached for the first boot only.
sudo mkdir -p /var/lib/libvirt/images/base
sudo curl -fsSL --max-time 600 -o /var/lib/libvirt/images/base/fedora-cloud.qcow2 \
https://download.example.com/Fedora-Cloud-Base-Generic.x86_64.qcow2 # keep the base read-only; never boot it directly
# Thin clone: a new qcow2 whose backing file is the base image, resized to 40G (guest grows the root fs on first boot via cloud-init)
sudo qemu-img create -f qcow2 -b /var/lib/libvirt/images/base/fedora-cloud.qcow2 -F qcow2 \
/var/lib/libvirt/images/my-vm.qcow2 40G# user-data.yaml: NoCloud user-data. The first line must be "#cloud-config".
#cloud-config
hostname: my-vm
fqdn: my-vm.example.com
users:
- name: admin
groups: [wheel]
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAAC3... admin@example.com
ssh_pwauth: false # keys only; see /ssh/ for hardening the daemon
package_update: true
packages: [qemu-guest-agent] # lets virsh domifaddr --source agent, domfsfreeze and --mode agent work
runcmd:
- systemctl enable --now qemu-guest-agent
growpart: { mode: auto, devices: ["/"] } # expand the partition to the resized disk# network-config.yaml (optional): static addressing in Netplan v2 format
version: 2
ethernets:
id0:
match: { name: "en*" }
addresses: [192.0.2.10/24]
routes: [{ to: default, via: 192.0.2.1 }]
nameservers: { addresses: [192.0.2.53] }sudo virt-install \
--name my-vm \
--osinfo detect=on,require=off \ # detect from the disk; do not fail if it cannot (fedora-cloud is a fine explicit value)
--memory 4096 --vcpus 2 \
--cpu host-passthrough \ # guest sees the host CPU; fastest, but blocks migration to a different CPU model
--disk path=/var/lib/libvirt/images/my-vm.qcow2,bus=virtio,discard=unmap \
--network network=default,model=virtio \
--graphics none --console pty,target.type=serial \ # serial console only; cloud images log to ttyS0
--cloud-init user-data=user-data.yaml,network-config=network-config.yaml \
--import \ # boot from the disk; no installer, no --location or --cdrom
--noautoconsole # return immediately; attach later with virsh console--cloud-init with no suboptions generates a root password, prints it once and disables cloud-init after first boot; use user-data= for anything reproducible. The seed ISO is deleted from the domain after the first boot, so a second virsh start does not rerun the first-boot steps; to re-provision, remove /var/lib/cloud inside the guest or rebuild the disk. virt-install --print-xml emits the XML without creating anything, which is the fastest way to see what a flag does.
For an ISO install use --cdrom path.iso (interactive) or --location URL --extra-args 'inst.ks=https://config.example.com/my-vm.ks console=ttyS0' for a kickstart-driven unattended install over the serial console. osinfo-query os lists every valid --osinfo name; the value sets sensible defaults for disk bus, NIC model and firmware, which is why virt-install 4 refuses to run without it unless require=off is given.
Cloud images are often customised without booting them at all. virt-customize from libguestfs edits the image in an appliance:
sudo virt-customize -a /var/lib/libvirt/images/my-vm.qcow2 \
--install qemu-guest-agent,vim-enhanced \
--ssh-inject admin:file:/home/admin/.ssh/id_ed25519.pub \
--selinux-relabel # required after writing files into a SELinux guest, or it boots into permissive relabel
sudo virt-sysprep -a my-vm.qcow2 --operations defaults,-ssh-userdir # strip machine-id, logs, host keys before templatingConsole and access#
virsh console my-vm # serial console on ttyS0; the guest kernel needs console=ttyS0 and a getty on it
virsh console my-vm --force # kick another session off the console
virsh domdisplay my-vm # spice:// or vnc:// URI for graphical guests
virsh vncdisplay my-vm
virt-viewer --connect qemu:///system my-vm # graphical console client
virsh domifaddr my-vm # from the libvirt DHCP lease (default network only)
virsh domifaddr my-vm --source agent # every address the guest sees, via qemu-guest-agent
virsh domifaddr my-vm --source arp # from the host ARP table; works for bridged guests without an agent
virsh qemu-agent-command my-vm '{"execute":"guest-info"}' --pretty # is the agent up, and what it supports
virsh domfsinfo my-vm # mounted filesystems from inside the guestDetach from the console with Ctrl-]. A blank console after connecting is normal until the guest prints something; press Enter. Cloud images enable the serial getty by default; an ISO install of Fedora or RHEL needs console=ttyS0,115200 on the kernel command line (grubby --update-kernel=ALL --args='console=ttyS0,115200' inside the guest, then systemctl enable serial-getty@ttyS0).
Storage pools and volumes#
A pool abstracts where disks live so virt-install --disk size=20 and virsh vol-create-as can allocate without a path. The default pool is /var/lib/libvirt/images, a directory. LVM (logical), NFS (netfs), iSCSI, Ceph RBD and ZFS pools follow the same commands.
virsh pool-list --all --details
virsh pool-info default
virsh pool-define-as fast dir --target /srv/vm-fast # define a directory pool
virsh pool-build fast # mkdir with the right ownership
virsh pool-start fast; virsh pool-autostart fast
virsh pool-refresh default # pick up files copied in from outside libvirt
virsh vol-create-as default data.qcow2 50G --format qcow2 --prealloc-metadata # sparse qcow2 with metadata written
virsh vol-list default --details
virsh vol-info data.qcow2 --pool default
virsh vol-upload --pool default my-vm.qcow2 ./local.qcow2 # copy an image into a pool on a remote host over the libvirt connection
virsh vol-resize data.qcow2 80G --pool default # grow; --shrink is destructive and refused unless given
virsh vol-delete data.qcow2 --pool default # removes the file
virsh vol-wipe data.qcow2 --pool default # overwrite before delete on shared storage
virsh pool-define-as vg0 logical --source-name vg0 --target /dev/vg0 # existing volume group as a pool
virsh pool-define-as nfs-images netfs --source-host nas.example.com --source-path /export/images --target /mnt/imagesAttaching a volume to a guest:
virsh attach-disk my-vm /var/lib/libvirt/images/data.qcow2 vdb --subdriver qcow2 --cache none --persistent # live + config
virsh attach-disk my-vm /var/lib/libvirt/images/data.qcow2 vdb --subdriver qcow2 --config # next boot only
virsh detach-disk my-vm vdb --persistent
virsh domblklist my-vm --details
virsh domblkinfo my-vm vda --human # capacity, allocation, physical; allocation grows as a qcow2 fills
virsh domblkerror my-vm # I/O errors QEMU has reported; a paused guest with "enospc" here means the pool is fullThe --subdriver qcow2 flag matters: without it libvirt treats a qcow2 file as raw, the guest sees the qcow2 header as its partition table and refuses to mount anything. cache=none with io=native is the usual choice for local disks on a journalled host filesystem; cache=writeback is faster and safe only if the host never loses power mid-write.
Disk images with qemu-img#
qemu-img info --backing-chain my-vm.qcow2 # format, virtual size, disk size, backing files, snapshots
qemu-img create -f qcow2 -o preallocation=metadata,cluster_size=64k data.qcow2 100G
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay.qcow2 # thin clone; -F is required since QEMU 6
qemu-img resize my-vm.qcow2 +20G # grow; guest must be shut off for a file it has open, then grow the partition inside
qemu-img resize --shrink my-vm.qcow2 40G # shrink: destroys data past the new size; shrink the guest filesystem and partition first
qemu-img convert -p -f qcow2 -O raw my-vm.qcow2 my-vm.raw # -p progress; raw for LVM or ZFS zvol targets
qemu-img convert -p -O qcow2 -c my-vm.qcow2 my-vm-compact.qcow2 # -c compresses; also drops unused clusters, so this is how you compact
qemu-img convert -p -f vmdk -O qcow2 exported.vmdk imported.qcow2 # VMware import; also vdi, vhdx, vpc
qemu-img convert -p -O qcow2 -o compat=1.1,lazy_refcounts=on in.qcow2 out.qcow2
qemu-img check my-vm.qcow2 # leaked clusters, corruption; -r leaks / -r all repairs (back up first)
qemu-img rebase -b new-base.qcow2 -F qcow2 overlay.qcow2 # point an overlay at a different backing file, copying differing clusters
qemu-img rebase -u -b new-base.qcow2 -F qcow2 overlay.qcow2 # -u: unsafe, metadata-only; use when the base was only moved or renamed
qemu-img commit overlay.qcow2 # write overlay changes into its backing file (offline)
qemu-img measure -O qcow2 -f raw my-vm.raw # how big the qcow2 would be before converting
qemu-img compare a.qcow2 b.qcow2 # byte-for-byte equality, respecting sparsenessNever modify an image QEMU has open
qemu-img on a disk of a running guest corrupts it; QEMU holds a lock since 2.10 and refuses (Failed to get "write" lock), and -U (--force-share) bypasses that lock for reads only. Use virsh blockresize, virsh blockcommit and virsh snapshot-create-as for anything live.
After growing an image, grow the partition and filesystem inside the guest: growpart /dev/vda 4 && resize2fs /dev/vda4 (ext4) or xfs_growfs / (XFS), or let cloud-init’s growpart module do it on boot. From the host, virt-resize --expand /dev/sda4 old.qcow2 new.qcow2 does both into a new file while the guest is off; virt-filesystems --long -h -a my-vm.qcow2 shows the partitions to name.
Snapshots#
libvirt has two snapshot mechanisms and they behave differently.
An internal snapshot (virsh snapshot-create-as my-vm name on a qcow2 disk, no --disk-only) stores disk state and, for a running guest, RAM inside the qcow2 file. It is a single command to take and revert, but pauses the guest for as long as it takes to write memory, is qcow2-only, cannot be taken with raw or LVM disks, and blocks blockcommit and migration while it exists.
An external snapshot (--disk-only) creates a new overlay file per disk and switches the guest to writing there; the old file becomes read-only backing storage. It takes milliseconds, works with any format, and is what backup tooling uses. Reverting means editing the domain XML (or virsh snapshot-revert on libvirt 10.0+, which finally supports external snapshots), and cleaning up means merging the chain with blockcommit.
virsh snapshot-list my-vm --tree
virsh snapshot-create-as my-vm pre-upgrade "Before kernel upgrade" # internal, includes RAM if running
virsh snapshot-revert my-vm pre-upgrade # discards everything since; guest state returns to the snapshot
virsh snapshot-revert my-vm pre-upgrade --running # revert and boot even if the snapshot was of a shut-off guest
virsh snapshot-delete my-vm pre-upgrade
# External, disk-only, for a running guest: quiesce flushes guest filesystems through qemu-guest-agent
virsh snapshot-create-as my-vm backup-$(date +%F) --disk-only --atomic --quiesce --no-metadata \
--diskspec vda,file=/var/lib/libvirt/images/my-vm.backup.qcow2 \
--diskspec vdb,snapshot=no # leave the data disk out of the chain
virsh domblklist my-vm # vda now points at my-vm.backup.qcow2, backed by my-vm.qcow2
cp --sparse=always /var/lib/libvirt/images/my-vm.qcow2 /backup/my-vm-$(date +%F).qcow2 # the base is now quiescent; copy it
virsh blockcommit my-vm vda --active --pivot --verbose # merge the overlay back into the base and switch the guest to it
rm /var/lib/libvirt/images/my-vm.backup.qcow2 # only after pivot succeeded; domblklist must show the original path--no-metadata tells libvirt not to track the snapshot, which is what you want when you will merge it minutes later; a tracked external snapshot cannot be blockcommitted away without also snapshot-delete --metadata. --atomic fails the whole snapshot if any disk fails instead of leaving a half-switched chain. --quiesce needs the guest agent; without it the snapshot is crash-consistent, which is fine for journalled filesystems and wrong for databases.
virsh blockpull my-vm vda is the opposite direction: pull the backing file’s data into the overlay and make it standalone, which is how a thin clone becomes an independent image.
Networking#
libvirt’s default network is a NAT: a Linux bridge virbr0 with an address (192.168.122.1/24 out of the box), dnsmasq for DHCP and DNS, and iptables or nftables rules that masquerade guest traffic out the host’s default route. Guests reach the world and each other; nothing outside the host can reach a guest without a port forward.
virsh net-list --all
virsh net-info default
virsh net-dumpxml default
virsh net-dhcp-leases default # MAC, IP, hostname, expiry
virsh net-edit default # edit and redefine
virsh net-start default; virsh net-autostart default
virsh net-destroy default # stops the bridge; running guests lose connectivity
# Fixed address for a guest by MAC, without restarting the network (--live --config)
virsh net-update default add ip-dhcp-host \
'<host mac="52:54:00:12:34:56" name="my-vm" ip="192.168.122.10"/>' --live --config| Mode | XML | What you get |
|---|---|---|
| NAT | <forward mode='nat'/> on a libvirt network | Outbound only; DHCP and DNS from dnsmasq; works on laptops and Wi-Fi |
| Routed | <forward mode='route'/> | Guest subnet routed via the host without masquerading; the upstream router needs a route back |
| Isolated | no <forward> | Guests talk to each other and the host only |
| Bridged | <interface type='bridge'><source bridge='br0'/> | Guest is on the host’s LAN with its own MAC; needs a host bridge, does not work over Wi-Fi |
| macvtap | <interface type='direct'><source dev='eno1' mode='bridge'/> | LAN presence without a bridge; guest and host cannot talk to each other |
| Open | <forward mode='open'/> | libvirt creates the bridge and DHCP but adds no firewall rules; you manage filtering |
| User (passt) | <interface type='user'><backend type='passt'/> | Unprivileged qemu:///session networking without SLIRP’s limitations (libvirt 9.0+) |
A host bridge for bridged mode, with NetworkManager on Fedora or RHEL 9 (see iproute2 for the underlying model):
nmcli con add type bridge ifname br0 con-name br0 ipv4.method auto
nmcli con add type bridge-slave ifname eno1 master br0 # moves the host's address onto br0; do this on a console, not over SSH via eno1
nmcli con up br0<!-- virt-install --network bridge=br0,model=virtio produces this -->
<interface type='bridge'>
<source bridge='br0'/>
<model type='virtio'/>
<mac address='52:54:00:aa:bb:cc'/> <!-- 52:54:00 is the QEMU/KVM prefix; keep it stable so DHCP reservations survive rebuilds -->
</interface>macvtap mode='bridge' is the quick alternative when you cannot rebuild the host’s network configuration. Its known limitation is that the guest and the host it runs on cannot exchange packets (the NIC does not loop frames back), so a monitoring agent on the host cannot see the guest unless there is a second, NAT interface.
Port forwarding into a NAT guest is not a libvirt feature. Add an nftables DNAT rule on the host, or give the guest a second interface on a bridge. A libvirt network hook (/etc/libvirt/hooks/network) is the supported place to add rules when the network starts.
virsh domiflist my-vm # interface, type, source, model, MAC
virsh domifstat my-vm vnet0 # rx/tx counters
virsh attach-interface my-vm bridge br0 --model virtio --persistent
virsh detach-interface my-vm bridge --mac 52:54:00:aa:bb:cc --persistent
virsh domif-setlink my-vm vnet0 down # unplug the cable from the host sideCPU, memory and performance settings#
virsh capabilities | grep -A5 '<cpu>' # host CPU model and features libvirt can offer
virsh domcapabilities | grep -A3 'mode=.host-model'
virsh nodeinfo; virsh freecell --all # NUMA nodes and free memory per node
virsh vcpuinfo my-vm; virsh vcpupin my-vm 0 4 --live # pin vCPU 0 to host CPU 4
virsh numatune my-vm --nodeset 0 --live # keep guest memory on one NUMA node
virsh memtune my-vm --hard-limit 9G # cgroup cap on QEMU's RSS, guest RAM plus overhead
virsh blkdeviotune my-vm vda --total-iops-sec 2000 --live # throttle a noisy guest
virsh domblkstat my-vm vda --human<cpu mode='host-passthrough'/> gives the guest every host CPU flag and the best performance, but migration works only between identical CPUs. host-model picks the closest named model and is the safe default for a cluster; a named model such as Skylake-Server-noTSX-IBRS is what you use when hosts differ. Nested virtualisation (a KVM host inside the guest) needs host-passthrough or host-model plus the vmx or svm feature and kvm_intel nested=1 on the host.
Memory ballooning (<memballoon model='virtio'/>) lets setmem reclaim memory from a cooperating guest. Hugepages (<memoryBacking><hugepages/></memoryBacking>) remove TLB pressure for databases and DPDK guests at the cost of static allocation. virtio-blk is fine for one or two disks; virtio-scsi (--disk ...,bus=scsi --controller type=scsi,model=virtio-scsi) supports discard, more devices and passthrough of SCSI commands.
Device passthrough#
VFIO hands a whole PCI function to a guest: a GPU, NVMe drive or NIC. Requirements are IOMMU enabled in firmware, intel_iommu=on iommu=pt (or amd_iommu=on) on the host kernel command line, and that every device in the same IOMMU group is passed through together or unused.
grep -E 'svm|vmx' /proc/cpuinfo | head -1 # virtualisation extensions present
dmesg | grep -iE 'DMAR|IOMMU' | head # IOMMU enabled at boot
virsh nodedev-list --cap pci | head
virsh nodedev-dumpxml pci_0000_01_00_0 | grep -E 'iommuGroup|product|vendor' -A1
for d in /sys/kernel/iommu_groups/*/devices/*; do echo "$(basename "$(dirname "$(dirname "$d")")") $(basename "$d")"; done | sort -n # group -> device
virsh nodedev-detach pci_0000_01_00_0 # unbind from the host driver and bind to vfio-pci; the host loses the device now
virsh nodedev-reattach pci_0000_01_00_0 # give it back<hostdev mode='subsystem' type='pci' managed='yes'> <!-- managed='yes': libvirt detaches and reattaches around guest start/stop -->
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
</hostdev>virt-install --hostdev pci_0000_01_00_0 or --hostdev 01:00.0 adds the same. USB devices use --hostdev 046d:c52b (vendor:product) or type='usb' XML. A guest with a passed-through device cannot be live-migrated and needs <memoryBacking><locked/></memoryBacking> or a memtune --hard-limit above guest RAM, because VFIO pins all guest memory.
SR-IOV virtual functions are the network equivalent: echo 4 > /sys/class/net/eno1/device/sriov_numvfs, then <interface type='hostdev' managed='yes'> per VF, or a libvirt network with <forward mode='hostdev' managed='yes'><pf dev='eno1'/></forward> that hands out VFs automatically.
Live migration#
Migration moves a running guest between two libvirt hosts, copying memory pages until the remaining dirty set is small enough to finish within the downtime limit (default 300 ms). It needs the same storage path visible on both hosts (shared NFS, Ceph or iSCSI pool), or --copy-storage-all to stream the disks too, plus a compatible CPU model and identical bridge or network names.
# Preconditions
virsh -c qemu+ssh://kvm2.example.com/system capabilities >/dev/null # SSH and libvirt reachable; see /ssh/ for agent forwarding
virsh domblklist my-vm # every path must exist on the destination, or use --copy-storage-all
virsh dumpxml my-vm | grep -E "cpu mode|bridge=|network="
# Live migration over SSH with the disks on shared storage
virsh migrate --live --persistent --undefinesource --verbose my-vm qemu+ssh://kvm2.example.com/system
# --persistent define the guest on the destination (otherwise it is transient there and vanishes at shutdown)
# --undefinesource remove the definition from this host once complete
# Without shared storage: stream the disk images too. Slow, and the destination needs pre-created empty files of the same size.
virsh migrate --live --persistent --undefinesource --copy-storage-all my-vm qemu+ssh://kvm2.example.com/system
# Tuning for large or busy guests
virsh migrate-setmaxdowntime my-vm 1000 # allow a 1 s pause so it converges
virsh migrate-setspeed my-vm 1000 # cap at 1000 MiB/s
virsh migrate --live --auto-converge ... # throttle guest vCPUs when dirtying outpaces copying
virsh migrate --live --postcopy --postcopy-after-precopy ... # switch to post-copy after one pass; the guest runs on the destination immediately and faults pages back
virsh domjobinfo my-vm # progress from the source side
virsh domjobabort my-vm # cancel; the guest keeps running on the source--unsafe forces a migration libvirt believes is unsafe, typically cache=writeback on a disk it cannot prove is coherent across hosts; the result is silent disk corruption unless you know better. --tunnelled sends the memory stream through the libvirt connection instead of a direct QEMU-to-QEMU TCP connection, which avoids opening ports 49152-49215 between hosts at a performance cost; --tls encrypts the direct stream when both daemons have migrate_tls_x509_cert_dir configured. Offline migration (virsh migrate --offline --persistent) copies just the definition, and is how you move a shut-off guest whose disks are already on shared storage.
Editing XML#
virsh edit opens the persistent XML in $EDITOR, validates against the schema on save and redefines the domain. Changes take effect at the next boot (or next start after shutdown); the running QEMU process is unaffected except for the few --live operations above.
virsh edit my-vm # validate and redefine
virsh dumpxml my-vm --inactive > my-vm.xml && vim my-vm.xml && virsh define my-vm.xml # the same, scripted
virt-xml my-vm --edit --cpu host-model # targeted edits without opening an editor
virt-xml my-vm --edit --memory 8192,maxmemory=16384
virt-xml my-vm --add-device --disk /var/lib/libvirt/images/data.qcow2,bus=virtio
virt-xml my-vm --edit target=vda --disk cache=none,discard=unmap
virt-xml my-vm --remove-device --network mac=52:54:00:aa:bb:cc
virt-xml my-vm --edit --boot uefi # switch to OVMF; the guest's OS must have been installed for UEFI
virt-xml my-vm --edit --print-diff --graphics none # show the change without applying it
virsh domxml-to-native qemu-argv --domain my-vm | tr ' ' '\n' | head -40 # the QEMU command line libvirt generatesThe fields people edit most:
<domain type='kvm'>
<name>my-vm</name>
<title>Build runner 3</title> <!-- free text; shown by virsh list --title -->
<memory unit='GiB'>4</memory>
<maxMemory slots='4' unit='GiB'>16</maxMemory> <!-- ceiling for memory hotplug -->
<vcpu placement='static' current='2'>8</vcpu> <!-- 8 configured, 2 online; setvcpus --live up to 8 -->
<os firmware='efi'> <!-- auto-select OVMF; libvirt 5.3+ -->
<type arch='x86_64' machine='q35'>hvm</type>
<boot dev='hd'/>
</os>
<cpu mode='host-model' check='partial'/>
<clock offset='utc'/>
<on_crash>restart</on_crash>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='none' io='native' discard='unmap'/>
<source file='/var/lib/libvirt/images/my-vm.qcow2'/>
<target dev='vda' bus='virtio'/>
<serial>my-vm-root</serial> <!-- appears as /dev/disk/by-id/virtio-my-vm-root in the guest -->
</disk>
<channel type='unix'> <!-- qemu-guest-agent socket; without it --source agent and --quiesce fail -->
<target type='virtio' name='org.qemu.guest_agent.0'/>
</channel>
<rng model='virtio'><backend model='random'>/dev/urandom</backend></rng> <!-- stops entropy starvation at boot -->
</devices>
</domain>Anything libvirt does not model can be added under <qemu:commandline> with the xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' namespace on the root element. Guests defined this way are marked tainted in the logs and unsupported by distributors, but it is how you pass a raw QEMU option that has no XML yet.
Oneliners#
# Running domains with their IPs (needs qemu-guest-agent in the guests)
for d in $(virsh list --name); do printf '%-20s %s\n' "$d" "$(virsh domifaddr "$d" --source agent 2>/dev/null | awk '/ipv4/ && !/127\.0/ {print $4}' | paste -sd,)"; done
# Shut every running guest down cleanly and wait up to 2 minutes
for d in $(virsh list --name); do virsh shutdown "$d"; done; timeout 120 sh -c 'while [ -n "$(virsh list --name)" ]; do sleep 2; done'
# Start every domain marked autostart that is not running
comm -23 <(virsh list --autostart --all --name | sort) <(virsh list --name | sort) | xargs -r -n1 virsh start
# Disk paths of every domain, for backup scripts
for d in $(virsh list --all --name); do virsh domblklist "$d" --details | awk -v d="$d" '$2=="disk"{print d, $4}'; done
# Total allocated versus virtual size of every image in the default pool
for v in $(virsh vol-list default --details | awk 'NR>2 && $2=="file"{print $1}'); do virsh vol-info "$v" --pool default --bytes | awk -v v="$v" '/Capacity/{c=$2} /Allocation/{a=$2} END{printf "%-30s %6.1f/%6.1f GiB\n", v, a/2^30, c/2^30}'; done
# Snapshot chain of every disk (which overlays have never been committed)
for f in /var/lib/libvirt/images/*.qcow2; do qemu-img info --backing-chain "$f" 2>/dev/null | grep -E '^(image|backing file):' | paste -sd' ' ; done
# MAC to name mapping for DHCP reservations
virsh list --all --name | xargs -I{} sh -c 'virsh domiflist {} | awk -v d={} "/^ *vnet|^ *-/ && \$5 ~ /:/ {print \$5, d}"'
# Clone a shut-off guest with a new disk and MAC
virt-clone --original my-vm --name my-vm-2 --auto-clone
# Guest agent ping for every running guest; a timeout means the agent is not installed or the channel is missing
for d in $(virsh list --name); do printf '%-20s ' "$d"; virsh qemu-agent-command "$d" '{"execute":"guest-ping"}' --timeout 3 >/dev/null 2>&1 && echo ok || echo no-agent; done
# Freeze guest filesystems, take a storage-level snapshot on the host, thaw
virsh domfsfreeze my-vm && lvcreate -s -n my-vm-snap -L 5G vg0/my-vm; virsh domfsthaw my-vm
# Hot-plug a second NIC on the default network
virsh attach-interface my-vm network default --model virtio --live --config
# Per-domain CPU time and RSS from the host, sorted by RSS
virsh list --name | xargs -I{} virsh domstats {} --cpu-total --balloon | awk '/^Domain/{d=$2} /cpu.time/{t=$2/1e9} /balloon.rss/{printf "%-20s cpu=%8.0fs rss=%6.0f MiB\n", d, t, $2/1024}' | sort -k3 -t= -rn
# Watch a guest's disk I/O in MiB/s
watch -n1 'virsh domblkstat my-vm vda --human | grep -E "rd_bytes|wr_bytes"'
# Console log for a headless guest: point the serial port at a file
virt-xml my-vm --edit --serial file,path=/var/log/libvirt/qemu/my-vm-console.log
# Which host processes belong to which guest
ps -o pid,rss,args -C qemu-system-x86_64 | grep -o 'pid=[0-9]* .*guest=[^,]*' | awk '{print $1, $NF}'
# libvirt's own log for a guest: why QEMU exited
tail -50 /var/log/libvirt/qemu/my-vm.log
# Rebuild the default NAT network's firewall rules after a firewalld restart wiped them
virsh net-destroy default && virsh net-start default
# Change the CPU model of every shut-off domain to host-model
for d in $(virsh list --all --name --state-shutoff); do virt-xml "$d" --edit --cpu host-model; done
# Export a guest definition for version control, stripped of host-specific IDs
virsh dumpxml my-vm --inactive | sed -e '/<uuid>/d' -e '/<mac address/d' > my-vm.xml
# Import a guest from another host: copy the definition and the disk
virsh -c qemu+ssh://kvm1.example.com/system dumpxml my-vm > my-vm.xml && scp kvm1.example.com:/var/lib/libvirt/images/my-vm.qcow2 /var/lib/libvirt/images/ && virsh define my-vm.xml
# Check the host is fit for KVM (IOMMU, cgroups, hugepages, nested)
virt-host-validate qemu
# Guest exit codes and crash reasons across the fleet from the journal
journalctl -u virtqemud --since -1d | grep -E 'shutting down|crashed|terminated' | tail
# Create a 1 GiB raw disk from /dev/zero without writing zeros
qemu-img create -f raw scratch.img 1G
# Compact a qcow2 in place by trimming from inside the guest, then copying
virsh domfstrim my-vm && qemu-img convert -p -O qcow2 my-vm.qcow2 my-vm.compact.qcow2Scripts#
Back up every running guest with an external disk-only snapshot, copying the quiescent base image and committing the overlay back. Guests without the agent get a crash-consistent snapshot and a warning.
#!/usr/bin/env bash
# usage: vm-backup.sh DEST_DIR [domain...] (defaults to every running domain)
set -euo pipefail
export LIBVIRT_DEFAULT_URI=qemu:///system
dest=${1:?destination directory required}; shift
mapfile -t domains < <( (($#)) && printf '%s\n' "$@" || virsh list --name )
stamp=$(date +%Y%m%dT%H%M%S)
mkdir -p "$dest"
for d in "${domains[@]}"; do
[[ -n $d ]] || continue
quiesce=(--quiesce)
virsh qemu-agent-command "$d" '{"execute":"guest-ping"}' --timeout 3 >/dev/null 2>&1 || { echo "$d: no guest agent, crash-consistent snapshot" >&2; quiesce=(); }
mapfile -t disks < <(virsh domblklist "$d" --details | awk '$2=="disk" && $3!="-" {print $3":"$4}')
specs=()
for spec in "${disks[@]}"; do specs+=(--diskspec "${spec%%:*},file=${spec#*:}.snap-$stamp"); done
virsh snapshot-create-as "$d" "bk-$stamp" --disk-only --atomic --no-metadata "${quiesce[@]}" "${specs[@]}" >/dev/null
for spec in "${disks[@]}"; do
src=${spec#*:}
cp --sparse=always -- "$src" "$dest/$d-${spec%%:*}-$stamp.qcow2" # the base is read-only while the overlay is active
done
for spec in "${disks[@]}"; do
virsh blockcommit "$d" "${spec%%:*}" --active --pivot >/dev/null # merge and switch back
rm -f -- "${spec#*:}.snap-$stamp" # overlay is unused once pivoted
done
virsh dumpxml "$d" --inactive > "$dest/$d-$stamp.xml"
printf '%s: backed up %d disk(s)\n' "$d" "${#disks[@]}"
doneHealth report across a group of KVM hosts: domain count, running versus defined, free memory and any guest paused on an I/O error. Uses virsh -c over SSH so nothing is installed on the far side beyond libvirt.
#!/usr/bin/env bash
# usage: kvm-fleet-report.sh host1 host2 ...
set -euo pipefail
printf '%-22s %5s %5s %8s %s\n' HOST RUN ALL FREE_GiB ATTENTION
for h in "$@"; do
uri="qemu+ssh://$h/system"
if ! timeout 20 virsh -c "$uri" version >/dev/null 2>&1; then printf '%-22s %s\n' "$h" 'UNREACHABLE'; continue; fi
running=$(virsh -c "$uri" list --name | grep -c . || true)
all=$(virsh -c "$uri" list --all --name | grep -c . || true)
free_kib=$(virsh -c "$uri" nodememstats | awk '/^free/ {print $3}')
attention=()
for d in $(virsh -c "$uri" list --name --state-paused); do attention+=("paused:$d"); done
for d in $(virsh -c "$uri" list --name); do
if virsh -c "$uri" domblkerror "$d" 2>/dev/null | grep -q .; then attention+=("blkerror:$d"); fi
done
printf '%-22s %5s %5s %8.1f %s\n' "$h" "$running" "$all" "$(awk -v k="$free_kib" 'BEGIN{print k/2^20}')" "${attention[*]:-}"
doneProvision a guest from a cloud image in one step: thin clone, cloud-init seed with the caller’s SSH key, wait for an address, and print it. Python, standard library only, run on the KVM host.
#!/usr/bin/env python3
"""usage: vm-create.py NAME [--base PATH] [--size 40G] [--mem 4096] [--vcpus 2] [--net default]"""
import argparse, pathlib, subprocess, sys, tempfile, time
p = argparse.ArgumentParser()
p.add_argument("name")
p.add_argument("--base", default="/var/lib/libvirt/images/base/fedora-cloud.qcow2")
p.add_argument("--size", default="40G")
p.add_argument("--mem", type=int, default=4096)
p.add_argument("--vcpus", type=int, default=2)
p.add_argument("--net", default="default")
p.add_argument("--key", default=str(pathlib.Path.home() / ".ssh/id_ed25519.pub"))
a = p.parse_args()
disk = pathlib.Path("/var/lib/libvirt/images") / f"{a.name}.qcow2"
if disk.exists():
sys.exit(f"{disk} already exists")
key = pathlib.Path(a.key).read_text().strip()
seed = tempfile.NamedTemporaryFile("w", suffix="-user-data.yaml", delete=False) # virt-install reads it by path; removed below
user_data = pathlib.Path(seed.name)
seed.write(f"""#cloud-config
hostname: {a.name}
users:
- name: admin
groups: [wheel]
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys: ["{key}"]
ssh_pwauth: false
packages: [qemu-guest-agent]
runcmd: [systemctl enable --now qemu-guest-agent]
growpart: {{mode: auto, devices: ["/"]}}
""")
seed.close()
try:
subprocess.run(["qemu-img", "create", "-f", "qcow2", "-b", a.base, "-F", "qcow2", str(disk), a.size], check=True, capture_output=True)
subprocess.run(["virt-install", "--name", a.name, "--osinfo", "detect=on,require=off",
"--memory", str(a.mem), "--vcpus", str(a.vcpus), "--cpu", "host-model",
"--disk", f"path={disk},bus=virtio,discard=unmap", "--network", f"network={a.net},model=virtio",
"--graphics", "none", "--console", "pty,target.type=serial",
"--cloud-init", f"user-data={user_data}", "--import", "--noautoconsole"], check=True, timeout=120)
finally:
user_data.unlink(missing_ok=True)
for _ in range(60): # up to 2 minutes for the agent to come up
out = subprocess.run(["virsh", "domifaddr", a.name, "--source", "agent"], capture_output=True, text=True).stdout
addrs = [l.split()[3].split("/")[0] for l in out.splitlines() if "ipv4" in l and "127.0.0.1" not in l]
if addrs:
print(f"{a.name} {addrs[0]} ssh admin@{addrs[0]}")
break
time.sleep(2)
else:
sys.exit(f"{a.name} started but no address reported; try: virsh console {a.name}")Troubleshooting#
virt-host-validate qemu # KVM, IOMMU, cgroups, hugepages, nested checks with PASS/WARN/FAIL
systemctl status virtqemud.socket virtnetworkd virtstoraged # modular daemons on Fedora/RHEL 9+
journalctl -u virtqemud -u virtnetworkd --since -1h
tail -100 /var/log/libvirt/qemu/my-vm.log # QEMU's own stderr: device errors, "Permission denied", crashes
virsh domstate my-vm --reason # paused (ioerror) is a full pool; shut off (crashed) means QEMU died
ausearch -m avc -ts recent | grep qemu # SELinux denials on image files
ls -lZ /var/lib/libvirt/images/ # ownership and label; should be qemu:qemu and svirt_image_t or virt_image_t| Symptom | Cause | Fix |
|---|---|---|
virsh list shows nothing, but virt-manager shows guests | Talking to qemu:///session, guests are in qemu:///system (or vice versa) | virsh uri; set LIBVIRT_DEFAULT_URI=qemu:///system |
Failed to connect socket to '/var/run/libvirt/virtqemud-sock' | Daemon not running or user not in libvirt group | systemctl start virtqemud.socket; usermod -aG libvirt "$USER" and re-login |
Could not open '/path/disk.qcow2': Permission denied | Image owned by root with mode 600, wrong SELinux label, or under a home directory QEMU cannot traverse | chown qemu:qemu, restorecon -Rv /var/lib/libvirt/images; move images out of /home or set security_driver = "none" in qemu.conf as a last resort (users) |
Guest boots but has no network on default | virbr0 down, dnsmasq not running, or firewalld reloaded and dropped libvirt’s rules | virsh net-start default; virsh net-destroy default && virsh net-start default after a firewalld restart |
| Bridged guest gets no DHCP lease | Host on Wi-Fi (no bridging), or bridge has no uplink slave, or upstream port security | bridge link show br0; confirm eno1 is a slave; use macvtap or NAT on Wi-Fi |
| Guest and host cannot ping each other | macvtap mode='bridge' design limitation | Add a second NAT interface, or use a real bridge |
virsh console hangs blank | No getty on ttyS0 or kernel missing console=ttyS0 | Press Enter; in the guest systemctl enable --now serial-getty@ttyS0 and add the kernel arg |
domifaddr --source agent errors Guest agent is not responding | qemu-guest-agent not installed or no org.qemu.guest_agent.0 channel in XML | Install the agent; virt-xml my-vm --add-device --channel unix,target.type=virtio,target.name=org.qemu.guest_agent.0 |
Guest paused with ioerror | Storage pool out of space or NFS gone away | Free space or restore the mount, then virsh resume my-vm; virsh domblkerror names the disk |
Guest sees a disk with no partitions after attach-disk | qcow2 attached as raw | Detach and re-attach with --subdriver qcow2 |
KVM is not available or slow guest | kvm_intel/kvm_amd not loaded, VT-x disabled in firmware, or running inside a VM without nested | lsmod | grep kvm; dmesg | grep kvm; on the outer host echo 1 > /sys/module/kvm_intel/parameters/nested and use --cpu host-passthrough |
Nested VM inside a guest fails with KVM: entry failed | Inner guest CPU model lacks vmx/svm | Outer guest needs <cpu mode='host-passthrough'/> or <feature policy='require' name='vmx'/> |
Requested operation is not valid: domain is already running on virsh start | Guest is running or in managedsave state | virsh domstate; virsh managedsave-remove if the saved image is stale |
Guest stuck in shutdown or virsh destroy does not return | QEMU blocked in D state on a dead NFS or iSCSI mount | Restore storage; failing that, kill -9 the QEMU PID from ps -C qemu-system-x86_64 and virsh destroy |
Live migration fails with unsupported configuration: guest CPU doesn't match | host-passthrough between different CPUs | Use host-model or a named model on both hosts; restart the guest to apply |
Migration fails with Unsafe migration: Migration may lead to data corruption | Disk cache is not none on non-coherent shared storage | Set cache='none', or --unsafe only when you have verified coherence |
Cannot access storage file ... No such file or directory on the destination | Disk path differs or storage not mounted there | Mount the same path, or --copy-storage-all with pre-created target files |
blockcommit refuses: disk vda has snapshots | libvirt tracks an external snapshot’s metadata | virsh snapshot-delete my-vm NAME --metadata, then commit |
| Clock drift in the guest | No kvm-clock, or guest paused for a long time | <clock offset='utc'><timer name='kvmclock'/></clock>; run chrony in the guest |
Unable to find any master var store for loader | UEFI guest defined with a fixed OVMF path that does not exist on this host | virt-xml my-vm --edit --boot uefi for firmware auto-selection, or install edk2-ovmf |
Further reading#
- libvirt domain XML format: every element
virsh editaccepts, with defaults. - libvirt network XML format: NAT, routed, open, isolated and hostdev networks.
- libvirt snapshots and live full disk backup: the external snapshot and blockcommit workflow.
- libvirt migration: transports, tunnelled versus direct, and what must match between hosts.
- virt-install(1) and virt-xml(1):
--cloud-init,--osinfo,--diskand--networksuboptions. - QEMU disk image utility:
qemu-imgsubcommands and format options. - cloud-init NoCloud datasource: what the seed ISO must contain.