Script to backup KVM VMs to backup location online
In order to move or copy the VM to a different server, you need to extract the qcow2 file to a disk location:
#!/bin/bash
# ----------- Configuration -----------
VM_NAME="your_vm_name"
[[ $# -gt 0 ]] && VM_NAME="$1"
BACKUP_DIR="/backups/kvm/${VM_NAME}"
TIMESTAMP=$(date +%F_%H-%M-%S)
SNAPSHOT_PREFIX="backup_snapshot_${TIMESTAMP}"
VM_XML_FILE="${BACKUP_DIR}/${VM_NAME}_config_${TIMESTAMP}.xml"
declare -a CREATED_SNAPSHOTS
# ------------------------------------
set -euo pipefail
# Cleanup handler in case of error or interrupt
cleanup_on_exit() {
echo "[!] Cleanup triggered due to error or interruption."
for SNAP_INFO in "${CREATED_SNAPSHOTS[@]}"; do
IFS='|' read -r DEVICE DISK <<< "$SNAP_INFO"
SNAP_PATH="${DISK}.snap"
if [[ -f "$SNAP_PATH" ]]; then
echo " ✘ Removing leftover snapshot: $SNAP_PATH"
rm -f "$SNAP_PATH"
fi
# Try to pivot back if snapshot is still active
echo " ↻ Attempting to blockcommit $DEVICE..."
virsh blockcommit "$VM_NAME" "$DEVICE" --active --pivot 2>/dev/null || true
done
echo "[!] Cleanup complete. Exiting."
}
trap cleanup_on_exit ERR INT
echo "[*] Starting backup of VM: $VM_NAME"
mkdir -p "$BACKUP_DIR"
# Step 0: Save VM XML
echo "[0] Saving VM configuration..."
virsh dumpxml "$VM_NAME" > "$VM_XML_FILE"
echo " ✔ Saved to $VM_XML_FILE"
# Step 1: Get disks
mapfile -t DISK_ENTRIES < <(virsh domblklist "$VM_NAME" --details | awk '$2 == "disk" {print $3 "|" $4}')
if [[ ${#DISK_ENTRIES[@]} -eq 0 ]]; then
echo "[!] No disks found for VM: $VM_NAME"
exit 1
fi
# Step 2: Snapshot
echo "[1] Creating snapshots..."
for ENTRY in "${DISK_ENTRIES[@]}"; do
IFS='|' read -r DEVICE DISK <<< "$ENTRY"
SNAP_PATH="${DISK}.snap"
if [[ -f "$SNAP_PATH" ]]; then
echo " ⚠ Snapshot file already exists: $SNAP_PATH (removing)"
rm -f "$SNAP_PATH"
fi
virsh snapshot-create-as --domain "$VM_NAME" \\
--name "$SNAPSHOT_PREFIX" \\
--no-metadata \\
--atomic \\
--disk-only \\
--diskspec "$DEVICE",snapshot=external,file="$SNAP_PATH"
CREATED_SNAPSHOTS+=("${DEVICE}|${DISK}")
echo " ✔ Created snapshot for $DEVICE ($DISK → $SNAP_PATH)"
done
# Step 3: Compress with zstd
echo "[2] Backing up disks with zstd..."
for ENTRY in "${DISK_ENTRIES[@]}"; do
IFS='|' read -r DEVICE DISK <<< "$ENTRY"
DISK_BASENAME=$(basename "$DISK")
BACKUP_FILE="${BACKUP_DIR}/${VM_NAME}_${DISK_BASENAME}_${TIMESTAMP}.qcow2.zst"
echo " → Compressing $DISK to $BACKUP_FILE"
zstd -T0 -19 -o "$BACKUP_FILE" "$DISK"
done
# Step 4: Blockcommit snapshots
echo "[3] Merging snapshots..."
for ENTRY in "${DISK_ENTRIES[@]}"; do
IFS='|' read -r DEVICE DISK <<< "$ENTRY"
virsh blockcommit "$VM_NAME" "$DEVICE" --active --pivot
echo " ✔ Committed snapshot for $DEVICE"
done
# Step 5: Remove snapshot files
echo "[4] Cleaning up snapshot files..."
for ENTRY in "${DISK_ENTRIES[@]}"; do
IFS='|' read -r DEVICE DISK <<< "$ENTRY"
SNAP_PATH="${DISK}.snap"
if [[ -f "$SNAP_PATH" ]]; then
rm -f "$SNAP_PATH"
echo " ✘ Removed $SNAP_PATH"
fi
done
# Reset trap
trap - ERR INT
echo "[✓] Backup completed successfully for VM: $VM_NAME"
zstd -d your_vm_disk.qcow2.zst -o your_vm_disk.qcow2
and import the vm definition with:
virsh define your_vm_config.xml