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.
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:
| Item | Ubuntu 26.04 | Rocky Linux 10.2 |
|---|---|---|
| libvirt / QEMU | 12.0.0 / 10.2.1 | 11.10.0 / 10.1.0 |
| Packages | qemu-system-x86 libvirt-daemon-system virtinst | qemu-kvm libvirt virt-install |
| Daemon model | monolithic libvirtd, started by libvirtd.socket | modular virtqemud, virtnetworkd, virtstoraged and friends |
| Right after install | socket active, sudo virsh works | sockets enabled but not started, sudo virsh fails |
| Plain user, same login session | Permission denied on libvirt-sock | silently uses qemu:///session, empty list |
Security driver (dominfo) | AppArmor, label libvirt-<uuid> | SELinux, label svirt_t |
| Default storage pool | none | none |
| Machine type virt-install picked | pc-q35-10.2 | pc-q35-rhel10.2.0 |
| Undefine error on a UEFI guest | cannot undefine domain with nvram | Cannot 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 --allfor peoplevirsh list --all --namefor loops in scriptsvirsh list --all --table --uuidfor names and UUIDs togethervirsh list --inactive,--autostart,--with-managed-saveto filter,--titleto 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:
| Command | Answers |
|---|---|
virsh domstate web01 --reason | State plus why: running (booted), shut off (destroyed), paused (user) |
virsh domblklist web01 --details | Disks and CD-ROMs with target and source file |
virsh domiflist web01 | NICs: host tap device, network, model, MAC |
virsh domblkinfo web01 vda --human | Virtual size vs space used on the host |
virsh dommemstat web01 | Balloon stats: actual, unused, available, rss |
virsh cpu-stats web01 --total | Total CPU, user and system seconds |
virsh domifstat web01 vnet5 | Bytes, packets, errors, drops for one NIC |
virsh domblkstat web01 vda --human | Read and write ops, bytes and latency totals |
virsh vcpuinfo web01 | Which host CPU each vCPU sits on, plus affinity |
virsh domstats web02 | Every counter group at once (119 lines for a 1 vCPU guest) |
virsh guestinfo web01 --os --hostname | Guest OS, kernel and hostname (needs the agent) |
virsh domfsinfo web01 | Mounted 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:
| Source | Command | Works when | What you get |
|---|---|---|---|
| lease (default) | virsh domifaddr web01 | Guest on a libvirt NAT or routed network | IPv4 with prefix |
| agent | virsh domifaddr web01 --source agent | qemu-guest-agent running in the guest | Every interface, IPv4 and IPv6, guest-side names |
| arp | virsh domifaddr web01 --source arp | Host 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:
| Command | What it does | State afterwards |
|---|---|---|
virsh start web01 | Boot a defined VM | running (booted) |
virsh start web01 --paused | Create the VM with vCPUs stopped | paused (user) |
virsh shutdown web01 | ACPI power button (or --mode agent) | shut off (shutdown) |
virsh reboot web01 | Clean guest reboot | running, reason unchanged |
virsh reset web01 | Hard reset, no guest shutdown | running, reason unchanged |
virsh destroy web01 | Hard power off, nothing deleted | shut off (destroyed) |
virsh suspend web01 | Freeze vCPUs, RAM stays allocated | paused (user) |
virsh resume web01 | Unfreeze | running (unpaused) |
virsh managedsave web01 | Save RAM to disk and stop the VM | saved in list --managed-save |
virsh start web01 after managedsave | Restore the saved RAM | running (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 thatlist --all --titleshowsvirsh domrename web01 web01-oldworks only on a shut off VM (cannot rename active domainotherwise)virsh start web01on a running VM fails witherror: Domain is already activevirsh start web01 --consoleneeds a terminal: without one it printsCannot run interactive console without a controlling TTYand 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:
| Flag | Use it when |
|---|---|
--nvram | The guest is UEFI and you want its varstore deleted |
--keep-nvram | The guest is UEFI and you plan to define it again |
--remove-all-storage | Delete the disk volumes listed in the XML |
--snapshots-metadata | The VM still has snapshots defined |
--checkpoints-metadata | The VM has backup checkpoints |
--managed-save | A managedsave image exists |
--tpm / --keep-tpm | The 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:
| Command | Result |
|---|---|
virsh vol-list default --details | Name, path, capacity and real allocation per volume |
virsh vol-info web01.qcow2 --pool default | Capacity 20.00 GiB, allocation 196.00 KiB |
virsh vol-resize web01-data.qcow2 8G --pool default | Size of volume 'web01-data.qcow2' successfully changed to 8G |
virsh vol-resize web01-data.qcow2 4G --pool default | Can't shrink capacity below current capacity unless shrink flag explicitly specified |
virsh vol-clone web01-data.qcow2 web01-data-copy.qcow2 --pool default | Full copy in the same pool |
virsh vol-dumpxml web01.qcow2 --pool default | Shows the <backingStore> path |
virsh vol-delete web01-data-copy.qcow2 --pool default | Vol web01-data-copy.qcow2 deleted |
virsh pool-destroy default | Stops 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:
| Flag | Running guest | Saved XML |
|---|---|---|
--live | changed now | unchanged, lost at next power off |
--config | unchanged | applies from next boot |
--persistent (or --live --config) | changed now | kept |
--current or no flag | live 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 --liveon a 2 vCPU guest:requested vcpus is greater than max allowable vcpus for the live domain: 4 > 2setvcpus web01 1 --live:unsupported configuration: failed to find appropriate hotpluggable vcpus to reach the desired target vcpu countsetmaxmem web02 2Gon 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:
| Snapshot | Command | What happened in the lab |
|---|---|---|
| Internal (default) | snapshot-create-as web01 --name before-nginx | BIOS guest: Location: internal, RAM included. UEFI guest: refused |
| External, disk only | snapshot-create-as web01 --name pre-upgrade --disk-only --atomic --quiesce | Worked on BIOS and UEFI, guest frozen through the agent |
| External, disk plus RAM | snapshot-create-as web01 --name live-state --memspec file=...mem,snapshot=external --diskspec vda,snapshot=external | 400 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/hostsis 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:
error: Cannot recv data: Host key verification failed.: Connection reset by peerbecause root on the source had never SSHed tokvm03by that name. Fix:sudo ssh root@kvm03 trueonce and accept the keyerror: cannot perform tunnelled migration without using peer2peer flagfrom--tunnelledon its own. Fix: add--p2p, or drop--tunnellederror: Unsafe migration: Migration without shared storage is unsafewith local disks. Fix:--copy-storage-allerror: Cannot access storage file '/var/lib/libvirt/images/web02-seed.iso': No such file or directorybecause 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.xmlbefore any changevirsh dumpxml web01 --xpath '//interface/mac/@address'for one valuevirsh edit web01to change the saved XML in$EDITOR, validated on savevirt-xml web01 --edit --cpu host-modelfor 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:
| Command | Shows |
|---|---|
virsh nodeinfo | Host CPUs, sockets, cores, threads, NUMA cells, RAM |
virsh nodememstats | Host total, free, buffers and cache in KiB |
virsh nodecpustats --percent | Host CPU usage split by user, system, iowait, guest |
virsh capabilities | Host CPU model and the security driver (selinux or apparmor) |
virsh domcapabilities | Emulator path, machine types and firmware this host can give a guest |
virsh hypervisor-cpu-models | CPU 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:
| virsh | Proxmox VE |
|---|---|
virsh list --all | qm list |
virsh start / shutdown / reboot | qm start / shutdown / reboot VMID |
virsh destroy (power off, keeps the VM) | qm stop VMID |
virsh undefine --remove-all-storage | qm destroy VMID |
virsh suspend / resume | qm suspend / resume VMID |
virsh dumpxml / edit | qm config VMID / qm set VMID ... |
virsh console (Ctrl+]) | qm terminal VMID (escape Ctrl+O, needs a serial device) |
virsh snapshot-create-as / snapshot-revert | qm snapshot / qm rollback |
virsh domifaddr --source agent | qm guest cmd VMID network-get-interfaces |
virsh migrate --live --copy-storage-all | qm migrate VMID NODE --online --with-local-disks |
virsh blockresize | qm disk resize VMID DISK SIZE |
virt-clone | qm 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.
Great job!!
Very helpful
Thanks for share it
Regards
Thanks a lot for positive comments.
Thanks for sharing the comprehensive virsh commands compilation.
This virsh commands cheatsheet sounds incredibly useful for managing KVM guests.
I found it really interesting how virsh, via Libvirt, supports such a wide array of hypervisors beyond KVM, like Xen, VirtualBox, and VMware vSphere ESX.
Appreciated! Nice post