How To

Virsh Commands Cheat Sheet: Manage KVM VMs from the CLI (PDF)

Most virsh cheat sheets predate two changes, modular libvirt daemons and external snapshots you can revert, and skip the NVRAM rule that blocks deleting UEFI guests. Run their commands on a 2026 KVM host and the first things you hit are a socket error, an NVRAM error and a snapshot that will not delete. This virsh commands cheat sheet is the list we keep open on our hypervisors: each command grouped by task, with the output you should see and the exact error when it goes wrong.

Original content from computingforgeeks.com - post 93628

It also covers three commands that can cost you data without a warning: blockcommit writing into a shared base image, vol-delete removing a backing file two VMs still use, and blockresize shrinking a disk. Every command shown with its output was run in October 2026 on Ubuntu 26.04 (libvirt 12.0, one libvirtd) and on Rocky Linux 10.2 (libvirt 11.10, modular daemons, SELinux enforcing).

Check which libvirt daemon answers virsh

Ubuntu 26.04 still runs one socket-activated libvirtd for everything. Rocky Linux 10 and RHEL 10 run a daemon per driver: virtqemud for VMs, virtnetworkd for networks, virtstoraged for pools and so on. Every virsh command on this page works the same against both, so the split only matters when something is down, and the error text then tells you which model the host uses. These are the differences we hit on the two lab hosts:

ItemUbuntu 26.04Rocky Linux 10.2
libvirt / QEMU12.0.0 / 10.2.111.10.0 / 10.1.0
Packagesqemu-system-x86 libvirt-daemon-system virtinstqemu-kvm libvirt virt-install
Daemon modelmonolithic libvirtd, started by libvirtd.socketmodular virtqemud, virtnetworkd, virtstoraged and friends
Right after installsocket active, sudo virsh workssockets enabled but not started, sudo virsh fails
Plain user, same login sessionPermission denied on libvirt-socksilently uses qemu:///session, empty list
Security driver (dominfo)AppArmor, label libvirt-<uuid>SELinux, label svirt_t
Default storage poolnonenone
Machine type virt-install pickedpc-q35-10.2pc-q35-rhel10.2.0
Undefine error on a UEFI guestcannot undefine domain with nvramCannot undefine domain with NVRAM/varstore

If you haven’t installed the hypervisor yet, follow the KVM install guide for Ubuntu 26.04 or the Rocky Linux and AlmaLinux KVM setup, then come back here.

Error: “No URI is provided and cannot identify any listening daemon socket path”

This is the first thing a fresh Rocky Linux 10 host prints. The packages enable the modular sockets, so they’d come up on the next boot, but the install does not start them, which leaves virsh with nothing to connect to:

sudo virsh list --all

The error names the cause and links the libvirt knowledge base page for it:

error: failed to connect to the hypervisor
error: Operation not supported: No URI is provided and cannot identify any listening daemon socket path to attempt to connect to. Please ensure the expected daemon sockets are active and/or provide an explicit URI. For more information see https://libvirt.org/kbase/failed_connection_after_install.html

Start every driver’s sockets in one loop instead of rebooting. Starting only virtqemud is not enough, because networks and pools live in their own daemons:

for drv in qemu network nodedev nwfilter secret storage interface; do sudo systemctl enable --now virt${drv}d.socket virt${drv}d-ro.socket virt${drv}d-admin.socket; done

Check the result. On Rocky the three modular sockets report active while libvirtd.socket stays inactive, and on Ubuntu it’s the other way round:

systemctl is-active virtqemud.socket virtnetworkd.socket virtstoraged.socket libvirtd.socket

On Rocky Linux 10.2 you should see three active sockets and an inactive libvirtd.socket. That pattern is how you tell a modular host from a monolithic one at a glance, so it is worth running before you debug anything else:

active
active
active
inactive

Now virsh version answers and prints the library, the API and the running QEMU in one go:

sudo virsh version

On the Rocky host the API line names the driver, not QEMU’s own version. The last line is the QEMU build that will actually run your guests, so compare that one when you move VMs between hosts:

Compiled against library: libvirt 11.10.0
Using library: libvirt 11.10.0
Using API: QEMU 11.10.0
Running hypervisor: QEMU 10.1.0

Run sudo virt-host-validate qemu next, because it catches a missing /dev/kvm or cgroup controller before your first VM fails to start. Every line should read PASS except the IOMMU and secure-guest checks. Those only matter for PCI passthrough and SEV or TDX guests, so the WARN they showed on both nested lab hosts is safe to ignore for everything on this page.

Error: “Failed to connect socket to ‘/var/run/libvirt/libvirt-sock’: Permission denied”

On Ubuntu the package adds every member of the sudo group to libvirt (its postinst loops over getent group sudo, and our journal logged usermod[34536]: add 'josphat' to group 'libvirt'), but a shell that was already open does not pick the group up. Plain virsh in that shell fails:

error: failed to connect to the hypervisor
error: Failed to connect socket to '/var/run/libvirt/libvirt-sock': Permission denied

Log out and back in, or wrap the command in sg libvirt -c 'virsh list --all' as we did, and the same user gets qemu:///system with no sudo. Group membership is read when you log in, so the shell you installed from keeps failing until then, even though getent group libvirt already lists you. Other users need sudo usermod -aG libvirt USER first, and the same new login.

VMs missing from virsh list: session vs system

On Rocky a plain user does not get an error at all. virsh quietly connects to the per-user qemu:///session, which has no VMs, so a full hypervisor looks empty:

virsh uri; virsh list --all

The URI gives it away. Your VMs are fine; you are simply looking at an empty per-user hypervisor:

qemu:///session

 Id   Name   State
--------------------

Pass -c qemu:///system, or set it once for the shell with export LIBVIRT_DEFAULT_URI=qemu:///system, which we tested on Ubuntu with a user in the libvirt group. To make it permanent for every shell, put uri_default = "qemu:///system" in ~/.config/libvirt/libvirt.conf; with that file in place, plain virsh uri printed qemu:///system for our non-root user. The same -c flag takes a remote host, as in virsh -c qemu+ssh://root@kvm03/system list --all, which the migration section uses and the remote KVM host guide covers in depth.

Create a VM: virsh create vs virsh define

virsh create file.xml starts a transient VM that libvirt forgets as soon as it stops. virsh define file.xml registers a persistent VM without starting it, and you boot it with virsh start. So if a quiz asks which command creates and starts a VM from an XML file, the answer is virsh create, but on a real host you almost always want define. The example assumes the default pool and Debian base image from the storage section and an existing VM, web01, to copy from. To show the difference, we copied web01’s XML into test01.xml with a new name and disk, and with no UUID or MAC so libvirt generates fresh ones:

sudo virsh vol-create-as default test01.qcow2 10G --format qcow2 --backing-vol debian-13-genericcloud-amd64.qcow2 --backing-vol-format qcow2 >/dev/null && sudo virsh dumpxml web01 --inactive --security-info | sed -e 's#<name>web01</name>#<name>test01</name>#' -e '/<uuid>/d' -e "/<mac address/d" -e 's#web01.qcow2#test01.qcow2#' -e '/<title>/d' -e '/<description>/d' > /tmp/test01.xml && grep -E '<name>|qcow2' /tmp/test01.xml

The grep confirms the new name and disk path made it in. If you skip deleting the UUID, the define step fails, because libvirt does not allow two VMs with the same UUID:

  <name>test01</name>
      <driver name='qemu' type='qcow2'/>
      <source file='/var/lib/libvirt/images/test01.qcow2'/>

Start it as a transient VM:

sudo virsh create /tmp/test01.xml

It boots straight away:

Domain 'test01' created from /tmp/test01.xml

Ask libvirt whether it will remember this VM:

sudo virsh dominfo test01 | grep -E 'Persistent|State'

Persistent: no is the transient marker:

State:          running
Persistent:     no

Undefining it fails, because there is no saved definition to remove:

sudo virsh undefine test01

libvirt says so plainly:

error: Failed to undefine domain 'test01'
error: Requested operation is not valid: cannot undefine transient domain

A virsh destroy test01 removes it from virsh list --all completely. Defining the same file gives you a VM that stays listed as shut off, with Persistent: yes:

sudo virsh define /tmp/test01.xml

Running the same define a second time doesn’t overwrite anything, because the name is taken:

error: Failed to define domain from /tmp/test01.xml
error: operation failed: domain 'test01' already exists with uuid 12c10356-d0bf-436b-b0c0-b90f1f23bb93

Most of the time you will not write XML by hand. virt-install --import builds it from a cloud image overlay, which is how both lab guests were made. Create the pool and the overlay first (see the storage section), and keep a short cloud-config file such as /tmp/ud-web02.yaml that creates your user, sets a password and installs qemu-guest-agent.

The cloud-config we used is ten lines (set your own password):

vim /tmp/ud-web02.yaml

It creates the login user and installs the guest agent the agent-backed commands below rely on:

#cloud-config
hostname: web02
users:
  - name: josphat
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: false
    plain_text_passwd: your-password-here
packages:
  - qemu-guest-agent
runcmd:
  - systemctl enable --now qemu-guest-agent

On the Ubuntu host, where --cloud-init did not apply, we wrote a two-line meta-data file and built the seed ISO by hand, then passed it to virt-install as --disk path=/var/lib/libvirt/images/web02-seed.iso,device=cdrom:

sudo apt install -y cloud-image-utils
printf "instance-id: web02\nlocal-hostname: web02\n" > /tmp/md-web02.yaml
sudo cloud-localds /var/lib/libvirt/images/web02-seed.iso /tmp/ud-web02.yaml /tmp/md-web02.yaml

With the file in place, build the VM:

sudo virt-install --name web02 --memory 1024 --vcpus 1 --import --boot uefi --disk vol=default/web02.qcow2,bus=virtio --os-variant debian13 --network network=default,model=virtio --graphics none --noautoconsole --cloud-init user-data=/tmp/ud-web02.yaml

On Rocky the --cloud-init seed applied on first boot; on Ubuntu the guest came up as localhost with no user, which is why we switched to the hand-built seed. The full workflow is in the cloud-init and virt-install guide. virt-install --osinfo list prints the valid --os-variant names. osinfo-query is a separate libosinfo tool, so virsh osinfo-query fails with unknown command. One more catch with the sed recipe above: it keeps the <nvram> path, so on a UEFI source add another -e that renames the _VARS.fd file, or both VMs share one varstore.

List VMs and read their state

virsh list without flags shows running VMs only, which is why a stopped guest seems to vanish. Add --all and pick the output form for the job:

  • virsh list --all for people
  • virsh list --all --name for loops in scripts
  • virsh list --all --table --uuid for names and UUIDs together
  • virsh list --inactive, --autostart, --with-managed-save to filter, --title to add a title column

The table form is the one to use when you need UUIDs, since plain --uuid prints bare UUIDs with no names:

sudo virsh list --all --table --uuid

Two running guests on the Rocky host. Stopped VMs show - in the Id column, because an Id only exists while a VM runs:

 Id   Name    State     UUID
--------------------------------------------------------------
 1    web01   running   003cbef2-3985-4961-ac2b-3af7d8890336
 2    web02   running   03e3211f-ad69-4e90-92b0-0c74cfc26d7a

dominfo is the one-screen summary. The security lines are worth reading, because they prove the guest is confined: SELinux with an svirt_t label on Rocky, AppArmor with a libvirt-<uuid> profile on Ubuntu. Autostart Once is a newer field you won’t see on older hosts:

sudo virsh dominfo web01

Rocky Linux 10.2 output. On Ubuntu the security lines read apparmor instead, and the rest is identical:

Id:             1
Name:           web01
UUID:           003cbef2-3985-4961-ac2b-3af7d8890336
OS Type:        hvm
State:          running
CPU(s):         2
CPU time:       22.2s
Max memory:     1048576 KiB
Used memory:    1048576 KiB
Persistent:     yes
Autostart:      disable
Autostart Once: disable
Managed save:   no
Security model: selinux
Security DOI:   0
Security label: system_u:system_r:svirt_t:s0:c239,c333 (enforcing)
Messages:       tainted: custom guest agent control commands issued

That tainted line appeared after we ran virsh qemu-agent-command against the guest, and qemu-monitor-command leaves tainted: custom monitor control commands issued the same way. Libvirt marks the VM because raw agent or monitor commands bypass its own state tracking. The taint is harmless in a lab, but it’s the first thing a support engineer asks about, so keep raw agent and monitor commands off production guests.

The rest of the inspection commands each answer one question, and all of them were run in the lab:

CommandAnswers
virsh domstate web01 --reasonState plus why: running (booted), shut off (destroyed), paused (user)
virsh domblklist web01 --detailsDisks and CD-ROMs with target and source file
virsh domiflist web01NICs: host tap device, network, model, MAC
virsh domblkinfo web01 vda --humanVirtual size vs space used on the host
virsh dommemstat web01Balloon stats: actual, unused, available, rss
virsh cpu-stats web01 --totalTotal CPU, user and system seconds
virsh domifstat web01 vnet5Bytes, packets, errors, drops for one NIC
virsh domblkstat web01 vda --humanRead and write ops, bytes and latency totals
virsh vcpuinfo web01Which host CPU each vCPU sits on, plus affinity
virsh domstats web02Every counter group at once (119 lines for a 1 vCPU guest)
virsh guestinfo web01 --os --hostnameGuest OS, kernel and hostname (needs the agent)
virsh domfsinfo web01Mounted guest filesystems (needs the agent)

The agent-backed rows need qemu-guest-agent running inside the guest. Before it starts, those commands fail with error: Guest agent is not responding: QEMU guest agent is not connected. On our Debian cloud guests the agent was up 90 to 120 seconds after first boot, once cloud-init had installed it.

Find a VM’s IP address with domifaddr

virsh domifaddr reads the DHCP lease by default, so it only works for guests on a libvirt-managed network like default. Bridged guests get their address from your LAN’s DHCP server, which libvirt cannot see, and the default output is an empty table. Pick the source that matches how the guest is connected:

SourceCommandWorks whenWhat you get
lease (default)virsh domifaddr web01Guest on a libvirt NAT or routed networkIPv4 with prefix
agentvirsh domifaddr web01 --source agentqemu-guest-agent running in the guestEvery interface, IPv4 and IPv6, guest-side names
arpvirsh domifaddr web01 --source arpHost has talked to the guest recently (bridged guests too)IPv4 only, prefix shown as /0

The agent source is the most complete, because it asks the guest itself:

sudo virsh domifaddr web01 --source agent

Notice the guest’s own interface name, enp1s0, instead of the host’s vnet0:

 Name       MAC address          Protocol     Address
-------------------------------------------------------------------------------
 lo         00:00:00:00:00:00    ipv4         127.0.0.1/8
 -          -                    ipv6         ::1/128
 enp1s0     52:54:00:5c:c9:bb    ipv4         192.168.122.172/24
 -          -                    ipv6         fe80::5054:ff:fe5c:c9bb/64

For every guest on a network at once, ask the network instead of each VM. The lease table includes the hostname each guest sent:

sudo virsh net-dhcp-leases default

Both Rocky guests and their leases. The Hostname column is whatever the guest sent in its DHCP request, so a guest that never set one shows - there:

 Expiry Time           MAC address         Protocol   IP address           Hostname   Client ID or DUID
------------------------------------------------------------------------------------------------------------------------------------------------
 2026-10-21 09:49:33   52:54:00:00:e9:7d   ipv4       192.168.122.229/24   web02      ff:56:50:4d:98:00:02:00:00:ab:11:86:af:a9:30:38:0a:f8:cd
 2026-10-21 09:49:06   52:54:00:5c:c9:bb   ipv4       192.168.122.172/24   web01      ff:56:50:4d:98:00:02:00:00:ab:11:3a:40:f5:fe:2b:e6:54:c5

All three sources need a running VM. Against a stopped one, domifaddr prints error: Failed to query for interfaces addresses followed by error: Requested operation is not valid: domain is not running. Leases also outlive their VMs: on the Ubuntu host, net-dhcp-leases still listed leases for deleted guests, so don’t treat a row as proof the VM exists.

Start, stop, suspend and save VMs

The lifecycle commands are short, and the state reason after each one tells you what actually happened. destroy is the one that trips people: it pulls the virtual power cord, and the VM and its disks stay exactly where they were. Each command below is listed with the domstate --reason it left behind on the lab hosts. reboot and reset keep whatever reason the VM already had:

CommandWhat it doesState afterwards
virsh start web01Boot a defined VMrunning (booted)
virsh start web01 --pausedCreate the VM with vCPUs stoppedpaused (user)
virsh shutdown web01ACPI power button (or --mode agent)shut off (shutdown)
virsh reboot web01Clean guest rebootrunning, reason unchanged
virsh reset web01Hard reset, no guest shutdownrunning, reason unchanged
virsh destroy web01Hard power off, nothing deletedshut off (destroyed)
virsh suspend web01Freeze vCPUs, RAM stays allocatedpaused (user)
virsh resume web01Unfreezerunning (unpaused)
virsh managedsave web01Save RAM to disk and stop the VMsaved in list --managed-save
virsh start web01 after managedsaveRestore the saved RAMrunning (restored)

Managed save is the underused one. For a booted 1 GB Debian guest whose QEMU process used about 480 MiB of host RAM, it wrote a 411 MB state file to /var/lib/libvirt/qemu/save/web02.save, and virsh start brought the guest back in 0.98 seconds as running (restored). The file is roughly the guest’s used RAM, so check free space under /var/lib/libvirt/qemu/save before saving big VMs. Save a paused VM and it comes back paused: one we saved before its vCPUs ever ran took 20 seconds to restore, landed in paused (migrating), and needed virsh resume.

shutdown only asks the guest. It returns at once, and a guest that’s still booting or ignores ACPI just keeps running. In scripts, follow it with virsh await, which waits for a state instead of polling domstate in a loop:

sudo virsh shutdown web01 --mode agent; sudo virsh await web01 --condition domain-inactive --timeout 90; sudo virsh domstate web01 --reason

A clean shutdown finishes inside the timeout, and you get the final state in the same run:

Domain 'web01' is being shutdown


shut off (shutdown)

When the guest ignores the request, await prints event loop timed out and exits 2, which is your cue for virsh destroy. The other condition, --condition guest-agent-available, waits until the agent answers, which is the right gate before shutdown --mode agent or domifaddr --source agent. We hit that in the lab: a shutdown sent seconds after boot was ignored, and the same command after await ... guest-agent-available worked.

Autostart now has a one-shot form. --once starts the VM on the next host boot only, which is handy before planned maintenance:

sudo virsh autostart web01 --disable; sudo virsh autostart web01 --once; sudo virsh dominfo web01 | grep -i autostart

The two flags show up as separate fields, so you can tell a permanent autostart from a one-time one:

Domain 'web01' unmarked as autostarted

Domain 'web01' marked as autostarted on next boot

Autostart:      disable
Autostart Once: enable

To clear it, use virsh autostart web01 --disable --once. You’ll want the one-shot form when a host reboot is planned and only some guests should come back with it. A few smaller lifecycle commands round this out:

  • virsh desc web01 --title --live --config "Nginx front end" sets a title that list --all --title shows
  • virsh domrename web01 web01-old works only on a shut off VM (cannot rename active domain otherwise)
  • virsh start web01 on a running VM fails with error: Domain is already active
  • virsh start web01 --console needs a terminal: without one it prints Cannot run interactive console without a controlling TTY and leaves the VM paused

That last one surprised us. Since libvirt 9.9, start --console attaches the console before starting the vCPUs so no boot output is lost. When the attach fails, the vCPUs never start, and the VM sits paused until you resume it.

Connect to the serial console and exit it

virsh console attaches to the guest’s first serial port, and Ctrl+] gets you out. That’s the plain Ctrl key with the right square bracket, no Shift, on a US layout. virsh prints the escape key every time you connect:

sudo virsh console web01

Press Enter once to get a login prompt. A guest that is still booting prints its boot log here instead:

Connected to domain 'web01'
Escape character is ^] (Ctrl + ])


web01 login: 

If the console stays blank, the guest isn’t sending anything to a serial port. Cloud images enable console=ttyS0 already, while a desktop or hand-installed Linux guest needs it on the kernel command line or serial-getty@ttyS0 enabled. On keyboards where Ctrl+] is awkward, pick another escape key with virsh -e "^G" console web01, and virsh confirms it with Escape character is ^G (Ctrl + G).

Error: “Active console session exists for this domain”

Only one client can hold the console. A second virsh console, or one left behind by a dropped SSH session, gets this:

error: operation failed: Active console session exists for this domain

Take it over with --force:

sudo virsh console web01 --force

The new session connects normally, and the session you kicked out ends with error: stream had I/O failure. If a colleague sees that line, someone forced the console away from them; it is not a crash. Add --resume to also unpause a paused guest after attaching.

Delete a VM without leaving files behind

virsh undefine web01 removes the definition only. Disks stay on the host until you add --remove-all-storage, and a UEFI guest refuses to undefine at all until you say what to do with its NVRAM file. That rule is old (libvirt 1.2.9, 2014), but UEFI is now the default for many guests, so more people hit it. Shut the VM down first, then pick the flags:

FlagUse it when
--nvramThe guest is UEFI and you want its varstore deleted
--keep-nvramThe guest is UEFI and you plan to define it again
--remove-all-storageDelete the disk volumes listed in the XML
--snapshots-metadataThe VM still has snapshots defined
--checkpoints-metadataThe VM has backup checkpoints
--managed-saveA managedsave image exists
--tpm / --keep-tpmThe guest has a vTPM state directory

Error: “Cannot undefine domain with NVRAM/varstore”

Rocky Linux 10.2 prints the newer wording and Ubuntu 26.04 the older one, but the cause is the same: a UEFI guest with an NVRAM file and neither --nvram nor --keep-nvram on the command line.

error: Failed to undefine domain 'web02'
error: Requested operation is not valid: Cannot undefine domain with NVRAM/varstore

Ubuntu’s version reads error: Requested operation is not valid: cannot undefine domain with nvram. Pass one of the two flags, not both, because virsh rejects the pair with error: Options --nvram and --keep-nvram are mutually exclusive.

Error: “cannot delete inactive domain with 1 snapshots”

A VM with snapshot metadata can’t be undefined either. Deleting each snapshot first works, but one flag does it. This full cleanup removed our broken test VM in one go:

sudo virsh undefine web01 --nvram --snapshots-metadata --remove-all-storage

Read what it removed, because the list is shorter than you might expect:

Domain 'web01' has been undefined
Volume 'vda'(/var/lib/libvirt/images/web01.pre-upgrade) removed.
Volume 'sda'(/var/lib/libvirt/images/web01-seed.iso) removed.

--remove-all-storage deletes the files the XML points at, which is the top of each disk’s chain. The VM had an external snapshot, so vda pointed at the overlay web01.pre-upgrade, and the layer underneath it, web01.qcow2, stayed in the pool as an orphan. The shared base image under every guest also survives, which is what you want. Run virsh vol-list default after any delete and remove leftovers by name.

Manage storage pools and volumes

Neither Ubuntu 26.04 nor Rocky Linux 10.2 created a storage pool at install, so virsh pool-list --all was empty on both and every vol-* command failed until we made one. A directory pool is four commands:

sudo virsh pool-define-as default dir --target /var/lib/libvirt/images
sudo virsh pool-build default
sudo virsh pool-start default
sudo virsh pool-autostart default

Each prints a one-line confirmation, and pool-list --details then shows capacity at a glance:

 Name      State     Autostart   Persistent   Capacity    Allocation   Available
----------------------------------------------------------------------------------
 default   running   yes          yes          28.38 GiB   2.82 GiB     25.57 GiB

LVM, NFS, iSCSI and ZFS pools use the same commands with a different type, covered in the KVM storage pool series. If you copy files into the pool directory yourself, run virsh pool-refresh default so libvirt sees them.

Volumes are the disks. The most useful form is a thin overlay on a cloud image, so every VM starts from the same base and only stores its own changes. Download the base into the pool directory first, then refresh the pool so libvirt sees it:

sudo wget -q -O /var/lib/libvirt/images/debian-13-genericcloud-amd64.qcow2 https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
sudo virsh pool-refresh default
sudo virsh vol-list default

The image shows up as a volume you can build on. This capture comes from the migration target, which already held a migrated guest’s disk:

Pool default refreshed

 Name                                 Path
--------------------------------------------------------------------------------------------------
 debian-13-genericcloud-amd64.qcow2   /var/lib/libvirt/images/debian-13-genericcloud-amd64.qcow2
 web02.qcow2                          /var/lib/libvirt/images/web02.qcow2

Now create a 20 GB overlay on top of it:

sudo virsh vol-create-as default web01.qcow2 20G --format qcow2 --backing-vol debian-13-genericcloud-amd64.qcow2 --backing-vol-format qcow2

A 20 GB disk that uses 196 KiB until the guest writes to it:

Vol web01.qcow2 created

The day-to-day volume commands, with what they printed in the lab:

CommandResult
virsh vol-list default --detailsName, path, capacity and real allocation per volume
virsh vol-info web01.qcow2 --pool defaultCapacity 20.00 GiB, allocation 196.00 KiB
virsh vol-resize web01-data.qcow2 8G --pool defaultSize of volume 'web01-data.qcow2' successfully changed to 8G
virsh vol-resize web01-data.qcow2 4G --pool defaultCan't shrink capacity below current capacity unless shrink flag explicitly specified
virsh vol-clone web01-data.qcow2 web01-data-copy.qcow2 --pool defaultFull copy in the same pool
virsh vol-dumpxml web01.qcow2 --pool defaultShows the <backingStore> path
virsh vol-delete web01-data-copy.qcow2 --pool defaultVol web01-data-copy.qcow2 deleted
virsh pool-destroy defaultStops the pool; files stay, but vol-list then fails with storage pool 'default' is not active

vol-delete removes a backing file that VMs still use

libvirt doesn’t check backing chains before it deletes a volume. We ran vol-delete on the Debian base image while two running VMs used it as their backing file, and it went straight through:

sudo virsh vol-delete debian-13-genericcloud-amd64.qcow2 --pool default

You get no prompt and no warning:

Vol debian-13-genericcloud-amd64.qcow2 deleted

Running guests keep the deleted file open, so nothing breaks at first. The damage shows the next time anything opens the chain: web01’s snapshot revert failed with Could not open backing file and left it shut off (failed), and web02 refused to boot with Cannot access backing file '/var/lib/libvirt/images/debian-13-genericcloud-amd64.qcow2'. Check a base image before deleting it with qemu-img info -U --backing-chain on each guest disk (-U lets it read disks that running VMs hold locked), or keep base images in a separate pool that nobody cleans up casually.

Attach and detach disks and network cards

Every hot-plug command takes a scope flag, and picking the wrong one is how a disk disappears after a reboot or never shows up in the running guest. attach-disk, attach-interface and their detach twins take all four forms below. change-media, setmem and setvcpus take --live, --config and --current, but not --persistent:

FlagRunning guestSaved XML
--livechanged nowunchanged, lost at next power off
--configunchangedapplies from next boot
--persistent (or --live --config)changed nowkept
--current or no flaglive if running, config if shut off

Create a data volume and attach it to both the running guest and its saved definition:

sudo virsh vol-create-as default web01-data.qcow2 5G --format qcow2
sudo virsh attach-disk web01 /var/lib/libvirt/images/web01-data.qcow2 vdb --driver qemu --subdriver qcow2 --targetbus virtio --persistent

domblklist shows vdb, and the guest agent’s guest-get-disks confirmed /dev/vdb inside Debian without a reboot:

 Target   Source
----------------------------------------------------
 vda      /var/lib/libvirt/images/web01.qcow2
 vdb      /var/lib/libvirt/images/web01-data.qcow2
 sda      /var/lib/libvirt/images/web01-seed.iso

With --live alone, the same attach worked but virsh dumpxml web01 --inactive | grep -c web01-data returned 0, so the disk would have been gone after the next shutdown. Detach with the same scope you attached with (virsh detach-disk web01 vdb --persistent). Detaching twice tells you exactly what’s wrong: error: No disk found whose source path or target is vdb.

Network cards follow the same pattern. Detach by MAC, because a guest can have several NICs on the same network:

sudo virsh attach-interface web01 --type network --source default --model virtio --persistent
sudo virsh detach-interface web01 --type network --mac 52:54:00:23:ec:56 --persistent

To swap or empty a CD-ROM, use virsh change-media web02 sda --eject --live --config, which prints Successfully ejected media. We needed exactly that before live migration, as you’ll see further down.

blockresize shrinks a disk without asking

virsh blockresize grows a running guest’s disk, and it shrinks one just as happily unless you pass --extend. We grew vda to 25 GB, then gave it a smaller number:

sudo virsh blockresize web01 vda 10G

The disk went from 25 GB to 10 GB with the same message a grow prints:

Block device 'vda' is resized

Anything the guest stored past the new end of the device is gone, and a partition table that still describes the old size points at blocks that no longer exist. libvirt 12.3 added blockresize --extend, which refuses to shrink, but it is opt-in: without the flag every release still shrinks. Neither lab host has it yet: both Ubuntu 26.04’s 12.0 and Rocky’s 11.10 answered error: command 'blockresize' doesn't support option --extend, so on those releases double-check the size argument before you press Enter. Growing the block device is also only half the job: the guest still has to grow its partition and filesystem, which the KVM disk resize guide walks through.

Change vCPUs and memory

You can change CPU and memory on a running guest only up to the maximums in its XML. Raising a maximum is always a --config change that waits for the next cold boot. Both limits produce a clear error when you go past them:

sudo virsh setmem web01 2G --live

The guest was defined with 1 GB, so 2 GB is over its maximum:

error: invalid argument: cannot set memory higher than max memory

Lowering memory works live through the balloon driver. After virsh setmem web01 768M --live, dominfo still showed 1047552 KiB for a moment, and dommemstat reported actual 786432 a few seconds later, once the balloon had inflated. The vCPU commands behave the same way:

sudo virsh setvcpus web01 4 --config --maximum
sudo virsh setvcpus web01 4 --config
sudo virsh setmaxmem web01 2G --config
sudo virsh vcpucount web01

vcpucount shows the gap between the saved config and the live guest until the next cold boot:

maximum      config         4
maximum      live           2
current      config         4
current      live           2

Three errors cover nearly every vCPU and memory change you’ll get wrong:

  • setvcpus web01 4 --live on a 2 vCPU guest: requested vcpus is greater than max allowable vcpus for the live domain: 4 > 2
  • setvcpus web01 1 --live: unsupported configuration: failed to find appropriate hotpluggable vcpus to reach the desired target vcpu count
  • setmaxmem web02 2G on a running guest: cannot resize the maximum memory on an active domain

The unplug error happens because vCPUs defined at boot aren’t hot-pluggable, so there’s nothing for libvirt to remove. Plan the boot count low and hot-add up to the maximum instead. Pinning works live: virsh vcpupin web01 0 1 --live moved vCPU 0 to host CPU 1, and virsh vcpupin web01 lists the affinity per vCPU, with emulatorpin and schedinfo covering the emulator threads and CPU shares.

Manage virtual networks

Both distros ship the default NAT network on virbr0 with DHCP for 192.168.122.2 to .254. net-list --all, net-info default and net-dumpxml default show its state, bridge and XML. The command most people need next is a fixed address for one guest, which net-update adds without restarting the network:

sudo virsh net-update default add ip-dhcp-host "<host mac='52:54:00:ad:c1:ee' name='web02' ip='192.168.122.50'/>" --live --config

It changes the running dnsmasq and the saved XML together:

Updated network default persistent config and live state

The guest picks up 192.168.122.50 at its next DHCP renewal or reboot. Adding the same entry again fails with there is an existing dhcp host entry in network 'default' that matches, so use net-update default delete ip-dhcp-host with the same XML to change it. For an isolated lab network with no route out, write the XML first:

vim isolated.xml

No <forward> element is what makes it isolated:

<network>
  <name>isolated</name>
  <bridge name='virbr10'/>
  <ip address='10.10.10.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='10.10.10.100' end='10.10.10.200'/>
    </dhcp>
  </ip>
</network>

Define, start and autostart it, then hot-plug a NIC into it:

sudo virsh net-define isolated.xml
sudo virsh net-start isolated
sudo virsh net-autostart isolated
sudo virsh attach-interface web02 --type network --source isolated --model virtio --live

Remove a network with net-destroy (stop) followed by net-undefine (delete). For bridged networking on the host’s real LAN instead of NAT, see KVM bridged networking with Netplan.

Restarting a network unplugs running guests

virsh net-destroy default followed by net-start default brings the network back, but running VMs don’t rejoin it. After that sequence on the Ubuntu host, web01’s tap device was no longer on the bridge, while virsh still reported its link as up:

ip -br link show master virbr0; sudo virsh domif-getlink web01 vnet5

Only the guest we had restarted since was attached:

vnet9            UNKNOWN        fe:54:00:ad:c1:ee <BROADCAST,MULTICAST,UP,LOWER_UP>
vnet5 up

Restarting the daemon that runs the VMs re-plugs every tap device without touching the guests, because it re-attaches each NIC as it reconnects to the running QEMU processes. On Ubuntu, sudo systemctl restart libvirtd put vnet5 back on virbr0 at once. On Rocky the network daemon is the wrong one: sudo systemctl restart virtnetworkd left the bridge empty, and sudo systemctl restart virtqemud brought vnet11 back.

Error: “network ‘default’ is not active”

A VM will not start while its network is down, because libvirt has no bridge to plug the tap device into. The error names the network, so you know which one to start. This is what web02 printed with default stopped:

error: Failed to start domain 'web02'
error: Requested operation is not valid: network 'default' is not active

Run sudo virsh net-start default, then start the VM again. Add sudo virsh net-autostart default as well, because a network that is not marked autostart stays down after every host reboot, and then every VM on it fails the same way. If net-start itself fails, a common cause is another interface already using 192.168.122.0/24 (libvirt reports the network as already in use by that interface), and the fix is a different range in net-edit default.

Take snapshots you can revert and delete

Plain snapshot-create-as still makes an internal snapshot, stored inside the qcow2 file, even on Rocky Linux 10, although the RHEL 10 virtualization guide it follows deprecates internal snapshots and supports only external ones. Internal snapshots also fail on UEFI guests and mix badly with external ones, so make --disk-only external snapshots your default:

SnapshotCommandWhat happened in the lab
Internal (default)snapshot-create-as web01 --name before-nginxBIOS guest: Location: internal, RAM included. UEFI guest: refused
External, disk onlysnapshot-create-as web01 --name pre-upgrade --disk-only --atomic --quiesceWorked on BIOS and UEFI, guest frozen through the agent
External, disk plus RAMsnapshot-create-as web01 --name live-state --memspec file=...mem,snapshot=external --diskspec vda,snapshot=external400 MB memory file, revert lands running (from snapshot)

Error: “internal snapshots of a VM with pflash based firmware require QCOW2 nvram format”

Both lab hosts printed this for a plain snapshot of a UEFI guest, because virt-install creates the NVRAM file in raw format:

error: Operation not supported: internal snapshots of a VM with pflash based firmware require QCOW2 nvram format

Add --disk-only --atomic and the same guest snapshots fine. Older libvirt, such as Ubuntu 24.04’s 10.0, prints internal snapshots of a VM with pflash based firmware are not supported for the same case.

Create, revert and delete an external snapshot

An external snapshot freezes the current disk and sends new writes to an overlay file. --quiesce asks the guest agent to flush and freeze filesystems first, so the snapshot is consistent:

sudo virsh snapshot-create-as web01 --name pre-upgrade --description "before apt full-upgrade" --disk-only --atomic --quiesce

Check the snapshot and where the disk points now:

sudo virsh snapshot-list web01; sudo virsh domblklist web01

The disk now points at the overlay:

 Name          Creation Time               State
----------------------------------------------------------
 pre-upgrade   2026-10-21 09:28:03 +0000   disk-snapshot

 Target   Source
-----------------------------------------------------
 vda      /var/lib/libvirt/images/web01.pre-upgrade
 sda      /var/lib/libvirt/images/web01-seed.iso

We then wrote a marker file in the guest and reverted:

sudo virsh snapshot-revert web01 --snapshotname pre-upgrade; sudo virsh domstate web01 --reason

A disk-only snapshot has no RAM to go back to, so the VM ends up powered off, and libvirt starts a fresh overlay named with a Unix timestamp:

Domain snapshot pre-upgrade reverted

shut off (from snapshot)

After virsh start web01 the marker file was gone, and domblklist showed vda on /var/lib/libvirt/images/web01.1792574884. virsh snapshot-delete web01 --snapshotname pre-upgrade then merged everything back, leaving vda on web01.qcow2 with no extra files. External revert landed in libvirt 9.9, so any host older than that can create these snapshots but not revert them.

Error: “deleting external snapshot that has internal snapshot as parent not supported”

Mixing the two kinds paints you into a corner. On Rocky we took an internal snapshot, then an external one on top, and libvirt refused to delete the external child:

error: Failed to delete snapshot after-change
error: Operation not supported: deleting external snapshot that has internal snapshot as parent not supported

There’s no clean way out except deleting the metadata with snapshot-delete ... --metadata and merging the files yourself. Keep each VM on one snapshot type and you never see this. virsh snapshot-list web01 --tree shows the parent and child order before you delete anything.

Merge overlays with blockcommit –shallow

virsh blockcommit merges an overlay back into the image below it, and --active --pivot does it on a running VM. Without --base or --shallow, though, it commits all the way to the bottom of the chain, and on a thin-provisioned host the bottom is the base image every other guest is built on. On the Rocky host, web02’s chain was overlay, then web02.qcow2, then the shared Debian base. We ran the command most guides show:

sudo virsh blockcommit web02 vda --active --pivot --verbose

It finished with Successfully pivoted, and domblklist showed where the data went:

 Target   Source
----------------------------------------------------------------------
 vda      /var/lib/libvirt/images/debian-13-genericcloud-amd64.qcow2
 sda      -

web02 now writes straight into the shared base image, and both its overlay and web02.qcow2 were committed into it. Every other VM built on that base now reads blocks that changed underneath it, which is how a golden image quietly corrupts a whole host. On the Ubuntu host the same default command was refused with Failed to get "write" lock, but that chain’s base had been deleted and restored earlier in the lab, so we can’t say for certain what held the lock. On Rocky nothing stopped it, so don’t count on image locking to save a shared base. Inspect the chain first with --xpath, then commit one layer only:

sudo virsh dumpxml web02 --xpath '//disk[target/@dev="vda"]//source/@file'

Three layers on the Ubuntu host, top first:

 file="/var/lib/libvirt/images/web02.pre-patch"
 file="/var/lib/libvirt/images/web02.qcow2"
 file="/var/lib/libvirt/images/debian-13-genericcloud-amd64.qcow2"

--shallow commits the top overlay into the layer directly below it and stops:

sudo virsh blockcommit web02 vda --active --shallow --pivot --verbose

The chain is back to web02.qcow2 on top of the untouched base:

Block commit: [99.99 %]
Block commit: [100.00 %]

Successfully pivoted

Clean up the snapshot metadata afterwards with snapshot-delete ... --metadata and remove the merged overlay file. For a chain deeper than two, --base names the exact layer to stop at.

If you want the longer treatment of libvirt storage, networking and the wider KVM stack behind these commands, Mastering KVM Virtualization (paid link) (second edition, Packt, 686 pages) is the most complete KVM book we know of. It was written in 2020, before modular daemons became the distro default and before external snapshot revert, so use it for the concepts and this page for the current command behavior.

Back up running VMs with backup-begin

virsh backup-begin copies a running VM’s disks without pausing it or leaving overlays behind, and checkpoints make the next copy incremental. It is the right tool for backups, while snapshots are for rollback. Create a checkpoint first, so libvirt starts tracking changed blocks:

sudo virsh checkpoint-create-as web01 --name cp1

Then describe the backup target. Open a file for the full backup:

vim backup-full.xml

Back up vda to a qcow2 file and skip the seed ISO:

<domainbackup mode='push'>
  <disks>
    <disk name='vda' backup='yes' type='file'>
      <target file='/var/lib/libvirt/images/web01-full.qcow2'/>
      <driver type='qcow2'/>
    </disk>
    <disk name='sda' backup='no'/>
  </disks>
</domainbackup>

Start it and watch the job:

sudo virsh backup-begin web01 backup-full.xml
sudo virsh domjobinfo web01

The job starts against the full 20 GiB virtual size, then only copies allocated data:

Backup started

Job type:         Unbounded
Operation:        Backup
Time elapsed:     126          ms
File processed:   0.000 B
File remaining:   20.000 GiB
File total:       20.000 GiB

The full backup came to 1.18 GiB on disk. For the incremental, copy the XML to backup-inc.xml, add <incremental>cp1</incremental> right after the opening <domainbackup> line, and point the target at web01-inc1.qcow2. Pass a second file holding <domaincheckpoint><name>cp2</name></domaincheckpoint> so the next run has a new starting point:

sudo virsh backup-begin web01 backup-inc.xml cp2.xml
sleep 10; sudo virsh domjobinfo web01 --completed | grep -E 'Job type|Operation|Time elapsed|File processed|File total'

Only the blocks changed since cp1 were copied, 4.375 MiB in just over a second:

Backup started

Job type:         Completed
Operation:        Backup
Time elapsed:     1158         ms
File processed:   4.375 MiB
File total:       4.375 MiB

virsh checkpoint-list web01 --tree showed cp1 with cp2 under it. Delete old checkpoints with checkpoint-delete web01 cp1 once their backups are rotated out, because every live checkpoint keeps a dirty bitmap in the disk. Running virsh backup-begin web01 with no XML also works, but it writes the copy next to the disk as <disk file>.<timestamp>, an 878 MB file we didn’t expect to find in the images directory.

Live migrate a VM to another KVM host

Live migration needs the same libvirt connection you’d use for any remote host, plus either shared storage or --copy-storage-all. We moved web02 between two Ubuntu 26.04 hosts with no shared storage. Before you start, line up the target:

  • root SSH key login from the source to the target
  • the target’s name resolving on the source (/etc/hosts is enough)
  • a storage pool on the target and the same network name active
  • the same CPU on both hosts, because virt-install defaults to host-passthrough

On the way we still hit four errors, in this order, and each one points to a step to do first:

  1. error: Cannot recv data: Host key verification failed.: Connection reset by peer because root on the source had never SSHed to kvm03 by that name. Fix: sudo ssh root@kvm03 true once and accept the key
  2. error: cannot perform tunnelled migration without using peer2peer flag from --tunnelled on its own. Fix: add --p2p, or drop --tunnelled
  3. error: Unsafe migration: Migration without shared storage is unsafe with local disks. Fix: --copy-storage-all
  4. error: Cannot access storage file '/var/lib/libvirt/images/web02-seed.iso': No such file or directory because the CD-ROM’s ISO only existed on the source. Fix: virsh change-media web02 sda --eject --live --config

With those fixed, the migration itself took four and a half seconds for a 1 GB guest:

time sudo virsh migrate --live --persistent --undefinesource --copy-storage-all --verbose web02 qemu+ssh://root@kvm03/system

You see progress lines, then the timing:

Migration: [ 1.64 %]
Migration: [ 3.36 %]
Migration: [ 3.96 %]
Migration: [ 5.53 %]
Migration: [ 5.99 %]
Migration: [100.00 %]
Migration: [100.00 %]
Migration: [100.00 %]


real	0m4.536s
user	0m0.017s
sys	0m0.014s

On the target, virsh domstate web02 --reason read running (migrated), and the guest kept its IP. --persistent defined it there and --undefinesource removed the definition from the source, so the VM exists on exactly one host. The old disk file stays on the source host, though, so clean it up yourself.

Two things about --copy-storage-all aren’t in the help text. The target got a standalone 1.18 GiB qcow2 with no backing file, so the thin overlay was flattened on the way. And the copy can’t be tunnelled: combining it with --p2p --tunnelled failed with migration of non-shared storage is not supported with tunnelled migration and this QEMU. The plain --p2p trip back to the first host worked, but it printed over 700 progress lines and left a fully allocated 20 GiB file there. libvirt 10.9 added --migrate-disks-detect-zeroes vda (it takes a disk list) to skip zero blocks during the copy; the manual says the disk needs discard='unmap' for it, and we didn’t test it here. VMs between unlike hosts are a different job; the KVM to Proxmox migration guide covers moving disks into Proxmox VE.

Edit, clone and inspect the domain XML

Every VM is an XML document, and four commands cover reading and changing it. Use dumpxml --inactive when you want the saved definition rather than the running one. They can differ while a guest runs: during virt-install’s first boot the live XML had <on_reboot>destroy</on_reboot>, and after it the saved XML read restart.

  • virsh dumpxml web01 > web01.xml before any change
  • virsh dumpxml web01 --xpath '//interface/mac/@address' for one value
  • virsh edit web01 to change the saved XML in $EDITOR, validated on save
  • virt-xml web01 --edit --cpu host-model for one setting without an editor

virsh edit applies to the next boot of a running VM, not the current one. Because it runs whatever EDITOR holds, it can also be scripted, which is how we changed the crash action in the lab:

sudo env EDITOR="sed -i 's|<on_crash>destroy|<on_crash>restart|'" virsh edit web02

It confirms the edit like an interactive session would:

Domain 'web02' XML configuration edited.

virsh has no clone command. virt-clone, from the same package as virt-install, copies the disks, generates a new MAC and UUID, and on UEFI guests gives the clone its own NVRAM file. The source has to be shut off, or it stops with ERROR Domain to clone must be shutoff.:

sudo virt-clone --original web02 --name web03 --auto-clone 2>&1 | tail -3

On Rocky it took 2.3 seconds, and the last three lines confirm the clone and its own NVRAM file:

Allocating 'web03_VARS.fd'                                  | 528 kB  00:00

Clone 'web03' created successfully.

The clone came out with MAC 52:54:00:4e:c3:79 against the original’s 52:54:00:00:e9:7d, and NVRAM at /var/lib/libvirt/qemu/nvram/web03_VARS.fd. On the Ubuntu host virt-clone refused with There is not enough free space on the storage pool to create the volume. (20483 M requested allocation > 13948 M available). It compares the source disk’s allocated size with the pool’s free space, and that disk had become fully allocated after the migration round trip. A thin source should pass that check, though we didn’t test it. Inside the guest, change the hostname and machine ID before both copies share a network.

Watch events and check the host

virsh event streams what libvirt does to your VMs, which beats polling when you debug a guest that keeps restarting or a script that changes state behind your back. We left it running while suspending, resuming, shrinking memory and rebooting web02:

sudo virsh event --all --loop --timestamp

Each action shows up within milliseconds, including the agent dropping during the reboot:

2026-10-21 09:44:01.983+0000: event 'lifecycle' for domain 'web02': Suspended Paused
2026-10-21 09:44:02.002+0000: event 'lifecycle' for domain 'web02': Resumed Unpaused
2026-10-21 09:44:02.019+0000: event 'balloon-change' for domain 'web02': 1047552KiB
2026-10-21 09:44:03.020+0000: event 'balloon-change' for domain 'web02': 921600KiB
2026-10-21 09:44:03.862+0000: event 'agent-lifecycle' for domain 'web02': state: 'disconnected' reason: 'channel event'
2026-10-21 09:44:04.878+0000: event 'reboot' for domain 'web02'
2026-10-21 09:44:06.494+0000: event 'rtc-change' for domain 'web02': 0

The host-side commands answer capacity and compatibility questions before you place or move a VM:

CommandShows
virsh nodeinfoHost CPUs, sockets, cores, threads, NUMA cells, RAM
virsh nodememstatsHost total, free, buffers and cache in KiB
virsh nodecpustats --percentHost CPU usage split by user, system, iowait, guest
virsh capabilitiesHost CPU model and the security driver (selinux or apparmor)
virsh domcapabilitiesEmulator path, machine types and firmware this host can give a guest
virsh hypervisor-cpu-modelsCPU models usable on this host (--all lists unusable ones too)
virsh qemu-monitor-command web02 --hmp 'info status'Raw QEMU monitor access; taints the VM

hypervisor-cpu-models is the quick check before migrating between different CPUs. Both nested lab hosts reported a Cascadelake-Server host model, so compare the two lists before moving a guest that pins a named CPU model. For a guest that needs to run its own hypervisor, the nested virtualization guide covers the vmx and svm passthrough this whole lab ran on.

Map virsh to qm on Proxmox VE

virsh doesn’t work on Proxmox VE, because Proxmox runs QEMU directly through its own qm tool instead of libvirt. A node in our lab running pve-manager 8.4.9 has the libvirt0 library, pulled in by libsys-virt-perl, but no virsh and no libvirt daemon. command -v virsh returns nothing, and libvirt couldn’t see Proxmox’s VMs even if you installed it. Use the qm equivalents:

virshProxmox VE
virsh list --allqm list
virsh start / shutdown / rebootqm start / shutdown / reboot VMID
virsh destroy (power off, keeps the VM)qm stop VMID
virsh undefine --remove-all-storageqm destroy VMID
virsh suspend / resumeqm suspend / resume VMID
virsh dumpxml / editqm config VMID / qm set VMID ...
virsh console (Ctrl+])qm terminal VMID (escape Ctrl+O, needs a serial device)
virsh snapshot-create-as / snapshot-revertqm snapshot / qm rollback
virsh domifaddr --source agentqm guest cmd VMID network-get-interfaces
virsh migrate --live --copy-storage-allqm migrate VMID NODE --online --with-local-disks
virsh blockresizeqm disk resize VMID DISK SIZE
virt-cloneqm clone VMID NEWID

We took the qm column from the qm manual and the PVE 8.4.9 node’s own qm help, not from runs on this page. Watch the one name that means opposite things, because it is the costly mistake. virsh destroy is a power-off that keeps the VM and its disks, while qm help destroy on that node reads “Destroy the VM and all used/owned volumes”. Muscle memory carried from a libvirt host to Proxmox deletes a VM, so use qm stop when you mean power off. If you’re moving guests the other way, the advanced virsh VM and host management guide picks up where this page stops.

Keep reading

Install KVM and Virt-Manager on Arch Linux Virtualization Install KVM and Virt-Manager on Arch Linux Install KVM on Debian 13 / Debian 12: Complete Guide KVM Install KVM on Debian 13 / Debian 12: Complete Guide Install VirtIO Drivers on Windows Server 2025 / Windows 11 KVM Install VirtIO Drivers on Windows Server 2025 / Windows 11 Best Mini PCs for ESXi 8 and VMware Homelabs Virtualization Best Mini PCs for ESXi 8 and VMware Homelabs Best Intel vPro Mini PCs for Free AMT KVM in a Homelab Proxmox Best Intel vPro Mini PCs for Free AMT KVM in a Homelab Best Mini PC for a Homelab, Proxmox, and Self-Hosting Proxmox Best Mini PC for a Homelab, Proxmox, and Self-Hosting

6 thoughts on “Virsh Commands Cheat Sheet: Manage KVM VMs from the CLI (PDF)”

Leave a Comment

Press ESC to close