Clone a VM in Virt-Manager with virt-clone and virt-sysprep

Cloning a VM copies bytes, not identity.

Page content

Virt-Manager (the libvirt GUI) copies a KVM disk and assigns the clone a UUID and MAC. The guest keeps its machine-id and SSH host keys until virt-sysprep clears them.

This walkthrough uses an Ubuntu 26.04 guest and a 14 GB qcow2 image stored at /mnt/extra2/vms. The disk commands are the same for other Linux guests. virt-sysprep does not operate on Windows guests.

Cloning a qcow2 disk between two KVM guests

The host already needs KVM and libvirt. Install KVM on Ubuntu 24.04 is that setup, and it pulls in virtinst, which provides virt-clone. virt-sysprep, virt-customize, virt-cat, and guestfish come from a separate package:

sudo apt install -y libguestfs-tools

What actually clones a VM

libvirt splits the job across three tools. virt-clone copies the disk images and defines a new domain with the same virtual hardware. It changes the domain name, the UUID, and the host-side MAC. Passwords, the hostname, static addresses, and files inside the guest are unchanged, which is the behaviour virt-clone(1) documents.

virt-sysprep opens the disk with libguestfs and edits the guest filesystem. The virt-sysprep(1) manual is explicit that the edit is in place: there is no output-file flag, so the image you pass to -a is the image that changes. Copy or clone first if you still need the source.

qemu-img info reports whether that file is standalone or the top of a backing chain. qemu-img convert writes a new file with the chain resolved.

flowchart LR A[guest shut down] -->|virt-clone| B[new domain and disk] B -->|qemu-img info| C{backing file?} C -->|yes| D[qemu-img convert] C -->|no| E[standalone qcow2] D --> E E -->|copy, then virt-sysprep| F[template] F -->|per-instance customize| G[imported guest]

Flatten before sysprep when a backing line is present. Sysprep the copy, then derive each running guest from that file.

Cloning the disk in Virt-Manager

Shut the guest down. virt-clone rejects a running domain, and a copy of a live qcow2 is not a consistent filesystem.

In Virt-Manager, right-click the guest and choose Clone. Set the new name. On the storage step, give each disk a new path. Leaving the original path in place defines a second domain that opens the same file, so both guests write the same blocks.

The new file lands beside the source. A default pool uses /var/lib/libvirt/images/. A disk that already lives on /mnt/extra2/vms is cloned into that directory. Note the path virt-clone prints, or read it back:

virsh domblklist ubuntu26.04-base14gb-clone

The command-line equivalent, with the name and the disk path chosen explicitly:

virt-clone --original ubuntu26.04-base14gb \
  --name ubuntu26.04-base14gb-clone \
  --file /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2

--auto-clone invents the name and the disk path. That is convenient for a throwaway copy. A template path is easier to keep when you pass --name and --file yourself. Omit --mac, or pass --mac RANDOM, and virt-clone generates an address in the QEMU range 52:54:00:xx:xx:xx.

--reflink asks for a lightweight copy. The Ubuntu 26.04 virt-clone manual limits that flag to raw images on the same btrfs filesystem, so it skips these qcow2 files. A qcow2 overlay is a separate setup, covered with qemu-img create -b below.

Does your clone have a backing file?

Run this on the new disk before you treat it as independent:

qemu-img info --backing-chain /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2

A standalone image lists file format, virtual size, and disk size. An overlay also lists a backing file: line pointing at the image it reads through. virtual size is the guest’s disk size, here about 14 GB. disk size is how much space the file currently occupies, which is small for a fresh overlay and grows as the guest writes.

virt-clone is not the usual source of that line. An overlay comes from qemu-img create -b, from a disk-only snapshot, or from a tool that created a qcow2 delta on purpose. The info check is what tells those cases apart from a full copy.

Keep the overlay only when the base will stay where it is and will not be written again. Every write from the clone lands in the small file; reads that miss in the overlay come from the base. Moving or editing the base breaks every guest that names it.

flowchart TB subgraph linked O[base.qcow2 read-only] C1[vm1.qcow2 deltas only] C2[vm2.qcow2 deltas only] C1 -->|backing file| O C2 -->|backing file| O end subgraph flat F1[vm1-flat.qcow2 standalone] F2[vm2-flat.qcow2 standalone] end

To make a standalone file, convert the overlay. -p prints progress. The destination is a new file; the overlay and the base are left as they are:

qemu-img convert -p -O qcow2 \
  /mnt/extra2/vms/ubuntu26.04-base14gb-clone.qcow2 \
  /mnt/extra2/vms/ubuntu26.04-base14gb-flat.qcow2

Run qemu-img info on the flat file and confirm there is no backing file: line. A template is that standalone file. Overlays are how you stamp guests from it later, not the template itself.

A btrfs reflink is a third case. cp --reflink=auto can share physical extents between two files, and qemu-img info still shows no backing file, because each path is a complete image. That copy is independent at the qcow2 layer even though the filesystem has not duplicated every block yet.

virt-sysprep: resetting the identity

Copy the standalone image, then sysprep the copy. --reflink=auto shares extents when the filesystem supports it and otherwise makes a normal copy, including on ext4. -n is a dry run that discards its writes:

cp --reflink=auto \
  /mnt/extra2/vms/ubuntu26.04-base14gb-flat.qcow2 \
  /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2

virt-sysprep -n -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2
virt-sysprep -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  --hostname ubuntu-template

On a default Ubuntu pool the image is often mode 0600 and owned by libvirt-qemu. The manual recommends giving your user write access rather than running as root. When the qemu user owns the file, sudo virt-sysprep -a ... is the invocation that can open it. The guest must be shut down, and nothing else should have the image open.

--hostname is a customize option, and customize is part of the default operation set, so it runs in the same invocation. There is no separate default operation that clears /etc/hostname on its own. --operations replaces that default set when you pass it; --operations defaults,-ssh-userdir keeps the defaults and turns one operation off.

These are the operations that decide whether the clone collides with the original. The list is the one in the current virt-sysprep manual, which is worth reading because a short --operations list silently drops the rest:

Operation Default Effect on the guest
machine-id yes Clears the machine ID. systemd generates a new one on the next boot when /etc/machine-id is empty.
ssh-hostkeys yes Removes /etc/ssh/ssh_host_*. The next boot creates new host keys.
ssh-userdir yes Removes .ssh under /root and /home/*, including authorized_keys and private keys stored in the guest.
dhcp-client-state yes Removes DHCP client leases.
udev-persistent-net yes Removes udev rules that map an old MAC to a fixed name such as eth0.
net-hwaddr yes Removes HWADDR from Fedora and RHEL ifcfg-* files. It does not edit Ubuntu netplan.
lvm-uuids, lvm-system-devices yes Assigns new LVM UUIDs and removes /etc/lvm/devices/system.devices, which otherwise pins the guest to the source disk’s WWIDs.
logfiles, bash-history, tmp-files yes Removes logs, shell history, and temp files.

ssh-userdir is the one that surprises people cloning a machine they still log into. The template should not carry the source’s private keys, so leave the operation on and inject a key per instance later. For a one-off copy that must keep the existing authorized_keys, pass --operations defaults,-ssh-userdir.

user-account is not in the default set, and passwords stay unless you pass --password or --root-password. fs-uuids is also off by default. The manual says it does not update every reference, including /etc/fstab, and that enabling it is likely to make the guest unbootable.

Ubuntu Server images usually have cloud-init, and virt-sysprep has no cloud-init operation. An old instance id under /var/lib/cloud makes the clone skip first-boot setup. Clean it on the template, and tell cloud-init to leave the hostname that --hostname wrote:

virt-customize -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  --run-command 'if command -v cloud-init >/dev/null 2>&1; then cloud-init clean --logs --seed; fi' \
  --write '/etc/cloud/cloud.cfg.d/99-preserve-hostname.cfg:preserve_hostname: true'

The same manual warns against cloning a guest that uses internal full-disk encryption for distribution: every copy shares the volume key. It also notes that deleted files are unlinked, not scrubbed. --scrub overwrites a named file, and virt-sparsify can drop freed space from the image.

How the clone gets its IP address

virt-sysprep has no operation that rewrites an address. A guest that used DHCP asks again. The clone has a new MAC, an empty machine-id, and no old lease file, so the DHCP server on libvirt’s default network treats it as a new client.

A static addresses: block in netplan is copied with the disk, and both guests then claim it. Read the files before the first boot. virt-ls and virt-cat are the one-shot tools; guestfish --ro -a <disk> -i is the same inspection as a shell, after which ls /etc/netplan and cat run against the guest:

virt-ls -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 /etc/netplan
virt-cat -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  /etc/netplan/00-installer-config.yaml

Installer images often use 00-installer-config.yaml or 01-netcfg.yaml. Cloud images use 50-cloud-init.yaml, which cloud-init will recreate unless network configuration is disabled. Netplan merges every YAML file in the directory, so a leftover static file still applies after you add a DHCP file.

To make the template a DHCP client, remove those files and upload one replacement. Use the interface name virt-cat showed. On a guest installed under KVM that name is often ens3 or enp1s0, and virt-clone keeps the same virtual PCI slot, so the name stays valid:

cat > /tmp/99-dhcp.yaml <<'EOF'
network:
  version: 2
  ethernets:
    ens3:
      dhcp4: true
EOF

virt-customize -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  --delete '/etc/netplan/*.yaml' \
  --upload /tmp/99-dhcp.yaml:/etc/netplan/99-dhcp.yaml \
  --write '/etc/cloud/cloud.cfg.d/99-disable-network.cfg:network: {config: disabled}'

For a static address, gateway, and DNS, use the same upload with the YAML from How to Change a Static IP Address in Ubuntu Server. Each instance then needs its own file, applied to the instance disk rather than the template.

The name in the YAML has to exist inside the guest. A disk moved from physical hardware often names the NIC enp2s0, while the same system under virtio presents a different name, and netplan then configures an interface that is not there. For a template that should boot on more than one machine type, pin the kernel to eth0 and use that key in netplan. Ubuntu sources /etc/default/grub.d/*.cfg while building grub.cfg:

virt-customize -a /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  --write '/etc/default/grub.d/99-eth0.cfg:GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX net.ifnames=0 biosdevname=0"' \
  --run-command 'update-grub'

update-grub runs inside the libguestfs appliance. If grub-probe cannot see the boot disk there, the command fails and the cmdline is unchanged. In that case, set the interface name in netplan to the name the guest already uses, and skip the eth0 switch.

Per-instance customization with virt-customize

Boot instances from copies of the clean file. Do not point a domain at the template itself.

A full copy has no runtime dependency on the template:

cp --reflink=auto \
  /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  /mnt/extra2/vms/ubuntu26.04-vm1.qcow2

An overlay stores only the blocks vm1 writes. -F qcow2 records the base format so QEMU does not have to probe it:

qemu-img create -f qcow2 \
  -b /mnt/extra2/vms/ubuntu26.04-base14gb-clean.qcow2 \
  -F qcow2 \
  /mnt/extra2/vms/ubuntu26.04-vm1.qcow2

The base path is stored inside the overlay. It has to keep that path, and it has to stay read-only, for every guest built this way.

The virt-sysprep security notes describe a second pass on each new disk. Sysprep writes a random seed into the template; guests cloned from that file would otherwise start with the same seed, which makes first-boot SSH host keys and TCP sequence numbers predictable. --enable customize rewrites the seed. --hostname and --ssh-inject are customize options, so they belong on the same command. Substitute the account that actually exists in the image for ubuntu:

virt-sysprep --enable customize \
  -a /mnt/extra2/vms/ubuntu26.04-vm1.qcow2 \
  --hostname vm1 \
  --ssh-inject "ubuntu:file:${HOME}/.ssh/id_ed25519.pub"

Hand the instance file to libvirt, not the template. If you created it as your user outside a storage pool, libvirt-qemu needs read access or the domain fails at startup with a permission error on the qcow2.

Importing into Virt-Manager

For each instance disk, open Virt-Manager, choose New Virtual Machine, and select Import existing disk image. Point it at ubuntu26.04-vm1.qcow2. The OS entry should be Ubuntu 26.04 when the host’s osinfo database has it.

The matching short id is ubuntu26.04. Check the host database before relying on it:

osinfo-query os | grep ubuntu26

If that prints nothing, the installed database is older than the 26.04 entry. --os-variant ubuntu24.04 still selects a virtio-friendly machine type for a guest that is already installed. --import tells virt-install the disk already contains an OS.

sudo virt-install --name ubuntu26.04-vm1 \
  --memory 4096 --vcpus 2 \
  --disk path=/mnt/extra2/vms/ubuntu26.04-vm1.qcow2,format=qcow2 \
  --import \
  --os-variant ubuntu26.04 \
  --network network=default \
  --noautoconsole

--network network=default attaches the NAT network whose dnsmasq instance hands out the DHCP lease. After boot:

virsh domifaddr ubuntu26.04-vm1 --source lease
virsh dumpxml ubuntu26.04-vm1 | grep "<mac"

--source lease reads that dnsmasq table. It stays empty for a static address, for a bridge, or for a NIC that is not on a libvirt virtual network. Those guests need qemu-guest-agent in the image and virsh domifaddr ubuntu26.04-vm1 --source agent, or an ARP lookup with --source arp. virsh net-dhcp-leases default lists every lease on the NAT network when you want the pool view rather than one domain.

Strategy Space per VM Time to create Constraint Use when
Full copy, or cp --reflink=auto about 14 GB logical; reflink shares blocks until they change minutes, or near-instant with reflink reflink needs a filesystem that supports it, such as btrfs a guest that should outlive the template
qcow2 overlay megabytes at first seconds base path must remain, and the base must stay read-only many similar guests from one sealed image
virt-clone --reflink raw image, shared extents near-instant raw disks on the same btrfs filesystem; qcow2 is skipped raw images already stored on btrfs

A guest you expect to keep for months is a full copy or a reflink. A short-lived guest can be an overlay, as long as the base file is still there when it runs. Proxmox is the other self-hosted path when you want a datacenter-style template and clone model on its own web UI. Multipass launches Ubuntu cloud images for a single user and applies cloud-init itself, which is a different workflow from sealing a disk you already installed.

Troubleshooting: when the clone will not boot

No address. virt-cat the netplan file and compare the interface key with ip link on the console. A match: macaddress entry still set to the source MAC drops the new NIC. A key that names an interface the guest does not have does the same. Fix the file on the disk, or apply the eth0 cmdline above and rebuild the instance from the template.

Address up, same as the original. The static addresses: block is still in one of the netplan files. virt-sysprep did not remove it. Upload a replacement and delete the other YAML files in /etc/netplan.

SSH says the host key changed, or accepts the clone as the original. Host keys are shared when ssh-hostkeys did not run on the file the domain opens. Confirm with virsh domblklist that the domain uses the sysprepped disk, then check qemu-img info so you did not sysprep an overlay while the guest boots from a different path. If the clone kept the original IP on purpose, clients that stored the old key in known_hosts will warn until that line is removed. That warning is the regeneration the manual describes.

Key login fails on a clone of a machine you could reach before. Default sysprep removed .ssh from the home directories. Inject a key with --ssh-inject, or re-run with --operations defaults,-ssh-userdir if this copy should have kept them.

LVM volumes missing after boot. The guest has /etc/lvm/devices/system.devices from the source, and the virtual disk’s WWID changed. The default lvm-system-devices operation removes that file. A hand-picked --operations list has to include it, and lvm-uuids, when the guest uses LVM.

One clone or a template

A single extra machine is virt-clone, a backing-file check, and virt-sysprep on the new disk. A fleet is one sealed standalone file, then a copy or an overlay per guest, with --enable customize on that guest’s own disk before it boots.

Overlays make the sealed file part of the runtime path of every guest that names it. Full copies and reflinks do not. Pick that dependency with the lifetime of the guest, and leave fs-uuids off so the bootloader and /etc/fstab still match the filesystem.

References

Subscribe

Get new posts on AI systems, Infrastructure, and AI engineering.