Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
I use this script to backup my QEMU/KVM/libVirt virtual machines. The script requires KVM 2.1+ since it uses the live blockcommit mode. This means the data in the snapshot disk is rolled back into the original instead of the other way around. Script does NOT handle spaces in paths.
#!/bin/bash
#
BACKUPDEST="$1"
DOMAIN="$2"
MAXBACKUPS="$3"
if [ -z "$BACKUPDEST" -o -z "$DOMAIN" ]; then
echo "Usage: ./vm-backup <backup-folder> <domain> [max-backups]"
exit 1
fi
if [ -z "$MAXBACKUPS" ]; then
MAXBACKUPS=6
fi
echo "Beginning backup for $DOMAIN"
#
# Generate the backup path
#
BACKUPDATE=`date "+%Y-%m-%d.%H%M%S"`
BACKUPDOMAIN="$BACKUPDEST/$DOMAIN"
BACKUP="$BACKUPDOMAIN/$BACKUPDATE"
mkdir -p "$BACKUP"
#
# Get the list of targets (disks) and the image paths.
#
TARGETS=`virsh domblklist "$DOMAIN" --details | grep ^file | awk '{print $3}'`
IMAGES=`virsh domblklist "$DOMAIN" --details | grep ^file | awk '{print $4}'`
#
# Create the snapshot.
#
DISKSPEC=""
for t in $TARGETS; do
DISKSPEC="$DISKSPEC --diskspec $t,snapshot=external"
done
virsh snapshot-create-as --domain "$DOMAIN" --name backup --no-metadata \
--atomic --disk-only $DISKSPEC >/dev/null
if [ $? -ne 0 ]; then
echo "Failed to create snapshot for $DOMAIN"
exit 1
fi
#
# Copy disk images
#
for t in $IMAGES; do
NAME=`basename "$t"`
cp "$t" "$BACKUP"/"$NAME"
done
#
# Merge changes back.
#
BACKUPIMAGES=`virsh domblklist "$DOMAIN" --details | grep ^file | awk '{print $4}'`
for t in $TARGETS; do
virsh blockcommit "$DOMAIN" "$t" --active --pivot >/dev/null
if [ $? -ne 0 ]; then
echo "Could not merge changes for disk $t of $DOMAIN. VM may be in invalid state."
exit 1
fi
done
#
# Cleanup left over backup images.
#
for t in $BACKUPIMAGES; do
rm -f "$t"
done
#
# Dump the configuration information.
#
virsh dumpxml "$DOMAIN" >"$BACKUP/$DOMAIN.xml"
#
# Cleanup older backups.
#
LIST=`ls -r1 "$BACKUPDOMAIN" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]+$'`
i=1
for b in $LIST; do
if [ $i -gt "$MAXBACKUPS" ]; then
echo "Removing old backup "`basename $b`
rm -rf "$b"
fi
i=$[$i+1]
done
echo "Finished backup"
echo ""
@vchakoshy

This comment has been minimized.

Copy link

commented Jun 16, 2016

Good job. ;)

@boulate

This comment has been minimized.

Copy link

commented Oct 26, 2016

Thank you for this share ! I was looking for a script to do exactly this kind of work without having to do it by hand.

Juste one thing : I think there is a mistake at the end of the script :

    if [ $i -gt "$MAXBACKUPS" ]; then
        echo "Removing old backup "`basename $b`
        rm -rf "$b"
    fi

Must be :

    if [ $i -gt "$MAXBACKUPS" ]; then
        echo "Removing old backup "`basename $b`
        rm -rf "$BACKUPDOMAIN/$b"
    fi

If you want the script to take the originial path in consideration. No ? :)

@ASAPHAANING

This comment has been minimized.

Copy link

commented Mar 2, 2017

Thank you very much for this useful utility script. I've added a wrapper that will handle the domains using virsh list --all and loop the backup script for all of those. By default this keeps 1 copy.

#!/bin/bash
#
# Get the list of all domains, active and inactive, iterate over the list with the main backup-script such that all domains get backed up.
test="$(virsh list --all | awk {'print $2'} | tail -n +3)"
while read -r line; do
    bash <vm-backup.sh location> <backup dir> "$line" 1
done <<< "$test"
@Monkybusiness

This comment has been minimized.

Copy link

commented Mar 7, 2017

Thanks for this script, very helpful. I had some problems though which turned out to be whenever there is a CDROM or FLOPPY in the hardware list for a VM. The problem is that the domblklist command used isn't filtered enough. It is filtered so only lines starting with "file" are used for building targets but the problem here is that things like a cdrom or a floppy will also be included and have no source. Output from the filtered domblklist can look like this:

Type Device Target Source

---------------------------------------------------

file disk hda /mnt/whatever/filename-flat.vmdk

file cdrom hdb -

This throws up an error as the snapshot command tries to create a snapshot for the cdrom without a source file. We need to get rid of any cdrom and floppy (could be other things, but this is what I had also starting with "file"), so I have substituted (in 3 places), where it says:

grep ^file

change to:

grep ^file | grep -v 'cdrom' | grep -v 'floppy'

After this, it works fine again. The alternative is of course to just remove all CDROM's and FLOPPY-units.

@hadrins

This comment has been minimized.

Copy link

commented Sep 17, 2017

Great script. It helped me get my own started. I need a script that would do a live backup on all the VM on the KVM. I was able to use your code and create a script that will backup all the running/paused VM.

I will have to fine tune it and post.
Thanks.

@shurak

This comment has been minimized.

Copy link

commented Jan 21, 2018

Great script and great comments. FYI your script is running in a nuclear facility :>

@GerhardK90

This comment has been minimized.

Copy link

commented Feb 28, 2018

Thank you for the script.
I agree with @boulate, without the modification the cleanup of old backups won't work properly.
Furthermore you should be aware, that the created backups might be in inconsistent states. While creating the snapshot it is not ensured, that the Disk is in a consistent state. This can lead to partial dataloss.
You should install qemu-agent on your VMs and run the snapshot with --quiesce.
Another option is to use virsh domfsfreeze and virsh domfsthaw.

@sebastiaanfranken

This comment has been minimized.

Copy link

commented Mar 21, 2018

I've used your script as a starting point. My script does a backup of all domains on the KVM server it's on. I run this every week for our weekly backup cycle. Works like a charm so far.

#!/bin/bash

# Set the language to English so virsh does it's output
# in English as well
LANG=en_US

# Define the script name, this is used with systemd-cat to
# identify this script in the journald output
SCRIPTNAME=kvm-backup

# List domains
DOMAINS=$(virsh list | tail -n +3 | awk '{print $2}')

# Loop over the domains found above and do the
# actual backup

for DOMAIN in $DOMAINS; do

	echo "Starting backup for $DOMAIN on $(date +'%d-%m-%Y %H:%M:%S')" | systemd-cat -t $SCRIPTNAME

	# Generate the backup folder URI - this is something you should
	# change/check
	BACKUPFOLDER=/mnt/$DOMAIN/$(date +%d-%m-%Y)
	mkdir -p $BACKUPFOLDER

	# Get the target disk
	TARGETS=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $3}')

	# Get the image page
	IMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')

	# Create the snapshot/disk specification
	DISKSPEC=""

	for TARGET in $TARGETS; do
		DISKSPEC="$DISKSPEC --diskspec $TARGET,snapshot=external"
	done

	virsh snapshot-create-as --domain $DOMAIN --name "backup-$DOMAIN" --no-metadata --atomic --disk-only $DISKSPEC 1>/dev/null 2>&1

	if [ $? -ne 0 ]; then
		echo "Failed to create snapshot for $DOMAIN" | systemd-cat -t $SCRIPTNAME
		exit 1
	fi

	# Copy disk image
	for IMAGE in $IMAGES; do
		NAME=$(basename $IMAGE)
		cp $IMAGE $BACKUPFOLDER/$NAME
	done

	# Merge changes back
	BACKUPIMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')

	for TARGET in $TARGETS; do
		virsh blockcommit $DOMAIN $TARGET --active --pivot 1>/dev/null 2>&1

		if [ $? -ne 0 ]; then
			echo "Could not merge changes for disk of $TARGET of $DOMAIN. VM may be in invalid state." | systemd-cat -t $SCRIPTNAME
			exit 1
		fi
	done

	# Cleanup left over backups
	for BACKUP in $BACKUPIMAGES; do
		rm -f $BACKUP
	done

	# Dump the configuration information.
	virsh dumpxml $DOMAIN > $BACKUPFOLDER/$DOMAIN.xml 1>/dev/null 2>&1

	echo "Finished backup of $DOMAIN at $(date +'%d-%m-%Y %H:%M:%S')" | systemd-cat -t $SCRIPTNAME
done

exit 0
@nebbian

This comment has been minimized.

Copy link

commented May 4, 2018

Hey @sebastiaanfranken, thanks for posting. I'm manually going through this script to see how it works, and am a little confused by these lines:

BACKUPIMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')

# Cleanup left over backups
	for BACKUP in $BACKUPIMAGES; do
		rm -f $BACKUP
	done

It seems to me that this script will delete the images from the running VMs, instead of the old backups. Is this a copy/paste issue from the lines above (getting the original list of images), or am I reading this wrong?

@tevkar

This comment has been minimized.

Copy link

commented May 12, 2018

@nebbian,

Setting BACKUPIMAGES before running blockcommit gives a list of snapshot image files which will be comitted. They can safely be removed if blockcommit operation was successful.

@mrmainnet

This comment has been minimized.

Copy link

commented May 24, 2018

Great ! Thank you for your script. But I think you should use '--spare=always' option with cp command.

@churusaa

This comment has been minimized.

Copy link

commented Jun 11, 2018

Updated version that uses rsync instead of cp so that progress, elapsed time, rate, etc... are shown. Example of pv included (not tested) not used because it doesn't preserve permissions.


# Set the language to English so virsh does it's output
# in English as well
# LANG=en_US

# Define the script name, this is used with systemd-cat to
# identify this script in the journald output
SCRIPTNAME=kvm-backup

# List domains
DOMAINS=$(virsh list | tail -n +3 | awk '{print $2}')

# Loop over the domains found above and do the
# actual backup

for DOMAIN in $DOMAINS; do

	echo "Starting backup for $DOMAIN on $(date +'%d-%m-%Y %H:%M:%S')" | systemd-cat -t $SCRIPTNAME

	# Generate the backup folder URI - this is something you should
	# change/check
	BACKUPFOLDER=/mnt/backups/$DOMAIN/$(date +%d-%m-%Y)
	mkdir -p $BACKUPFOLDER

	# Get the target disk
	TARGETS=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $3}')

	# Get the image page
	IMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')

	# Create the snapshot/disk specification
	DISKSPEC=""

	for TARGET in $TARGETS; do
		DISKSPEC="$DISKSPEC --diskspec $TARGET,snapshot=external"
	done

	virsh snapshot-create-as --domain $DOMAIN --name "backup-$DOMAIN" --no-metadata --atomic --disk-only $DISKSPEC 1>/dev/null 2>&1

	if [ $? -ne 0 ]; then
		echo "Failed to create snapshot for $DOMAIN" | systemd-cat -t $SCRIPTNAME
		exit 1
	fi

	# Copy disk image
	for IMAGE in $IMAGES; do
		NAME=$(basename $IMAGE)
                # cp $IMAGE $BACKUPFOLDER/$NAME
                # pv $IMAGE > $BACKUPFOLDER/$NAME
		rsync -ah --progress $IMAGE $BACKUPFOLDER/$NAME
	done

	# Merge changes back
	BACKUPIMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')

	for TARGET in $TARGETS; do
		virsh blockcommit $DOMAIN $TARGET --active --pivot 1>/dev/null 2>&1

		if [ $? -ne 0 ]; then
			echo "Could not merge changes for disk of $TARGET of $DOMAIN. VM may be in invalid state." | systemd-cat -t $SCRIPTNAME
			exit 1
		fi
	done

	# Cleanup left over backups
	for BACKUP in $BACKUPIMAGES; do
		rm -f $BACKUP
	done

	# Dump the configuration information.
	virsh dumpxml $DOMAIN > $BACKUPFOLDER/$DOMAIN.xml 1>/dev/null 2>&1

	echo "Finished backup of $DOMAIN at $(date +'%d-%m-%Y %H:%M:%S')" | systemd-cat -t $SCRIPTNAME
done

exit 0
@dalu

This comment has been minimized.

Copy link

commented Oct 28, 2018

@churusaa use rsync -ahW when you're not making a diff copy with rsync, it's a lot faster.
Also in some cases one would want to omit user and group ownership change, so the result is

rsync -ahW --no-o --no-g $IMAGE $BACKUPFOLDER/$NAME

@bttrfngrs

This comment has been minimized.

Copy link

commented Nov 19, 2018

@churusaa I was loving this script .. until a recent upgrade on our ubuntu to the latest Bionic Beaver the script quickly returns to prompt without executing any commands..

@andreaswork

This comment has been minimized.

Copy link

commented Jan 8, 2019

I had some problems with blockjob still being active when the script tried to blockcommit on VM's with more than 1 disk, had to manually abort the blockjob and redo blockcommit on both disks.

To prevent this issue, i added "--wait" option on line 60, so it actually checks and doesn't assume the blockjob is complete before starting blockcommit.

ps. this is not an issue with this script, this is an issue with Virsh tool, --wait fixes this problem.

@addictedtoflames

This comment has been minimized.

Copy link

commented Jan 19, 2019

Loving this script, I just have one issue with it. Some of my VMs have physical disks attached to them meaning that this script returns the error: "error: unsupported configuration: source for disk 'sda' is not a regular file; refusing to generate external snapshot name"

I already have backups in place for the files on these disks so I don't need them included in the snapshot. Is there a way I can exclude block devices within the script?

@berlinguyinca

This comment has been minimized.

Copy link

commented Feb 28, 2019

well done!

@reinistihovs

This comment has been minimized.

Copy link

commented Jul 25, 2019

Heres my version, which creates backups with rsync --sparse and --inplace.
What this means?
First time when script is running, the whole .qcow2 file is transfered,
Next time only the changes inside the .qcow2 file are synced into the backup file.
This makes the backup complete 10x faster.
This also saves a TON of space.

#!/bin/bash

#To exclude a domain, please add to its name "nobackup"
#First shutdown the guest, then use this command: virsh domrename oldname newname.

DATE=`date +%Y-%m-%d.%H:%M:%S`
LOG=/var/log/kvm-backup.$DATE.LOG
BACKUPROOT=/backup


DOMAINS=$(virsh list --all | tail -n +3 | awk '{print $2}')

for DOMAIN in $DOMAINS; do
        echo "-----------WORKER START $DOMAIN-----------" > $LOG
        echo "Starting backup for $DOMAIN on $(date +'%d-%m-%Y %H:%M:%S')"  >> $LOG

        if [[ $DOMAIN == *"nobackup"* ]];then
                echo "Skipping $DOMAIN , because its excluded." > $LOG
                exit 1
        fi

        VMSTATE=`virsh list --all | grep $DOMAIN | awk '{print $3}'`
        if [[ $VMSTATE != "running" ]]; then
                echo "Skipping $DOMAIN , because its not running." > $LOG
                exit 1
        fi

        BACKUPFOLDER=$BACKUPROOT/KVM-BACKUPS/$DOMAIN
        mkdir -p $BACKUPFOLDER
        TARGETS=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $3}')
        IMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')
        DISKSPEC=""
        for TARGET in $TARGETS; do
                DISKSPEC="$DISKSPEC --diskspec $TARGET,snapshot=external"
        done

        virsh snapshot-create-as --domain $DOMAIN --name "backup-$DOMAIN" --no-metadata --atomic --disk-only $DISKSPEC >> $LOG
        if [ $? -ne 0 ]; then
                echo "Failed to create snapshot for $DOMAIN" > $LOG
                exit 1
        fi

        for IMAGE in $IMAGES; do
                NAME=$(basename $IMAGE)
                if test -f "$BACKUPFOLDER/$NAME"; then
                echo "Backup exists, merging only changes to image" > $LOG
                rsync -apvz --inplace $IMAGE $BACKUPFOLDER/$NAME >> $LOG
                else
                echo "Backup does not exist, creating a full sparse copy" > $LOG
                rsync -apvz --sparse $IMAGE $BACKUPFOLDER/$NAME >> $LOG
                fi

        done

        BACKUPIMAGES=$(virsh domblklist $DOMAIN --details | grep disk | awk '{print $4}')
        for TARGET in $TARGETS; do
                virsh blockcommit $DOMAIN $TARGET --active --pivot >> $LOG

                if [ $? -ne 0 ]; then
                        echo "Could not merge changes for disk of $TARGET of $DOMAIN. VM may be in invalid state." > $LOG
                        exit 1
                fi
        done

        for BACKUP in $BACKUPIMAGES; do
                if [[ $BACKUP == *"backup-"* ]];then

                echo "deleted temporary image $BACKUP" > $LOG
                rm -f $BACKUP
                fi
        done

        virsh dumpxml $DOMAIN > $BACKUPFOLDER/$DOMAIN.xml
        echo "-----------WORKER END $DOMAIN-----------" >> $LOG
        echo "Finished backup of $DOMAIN at $(date +'%d-%m-%Y %H:%M:%S')" >> $LOG
done

exit 0

@fuznutz04

This comment has been minimized.

Copy link

commented Aug 2, 2019

@reinistihovs, I'll give your version a try. I have been using the original one posted in this thread, and it is successfully taking snapshots, transferring them etc. It is also generating a domain.xml file. However, the file is blank. If I manually do a dumpxml, it generates the proper configuration file. But it does not do so properly via the script. Any ideas?

@fuznutz04

This comment has been minimized.

Copy link

commented Sep 5, 2019

Does anyone else have this issue? This will generate a domain.xml file, but the file is blank. If I manually do a dumpxml, it generates the proper configuration file. But it does not do so properly via the script. Any ideas?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.