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.

Show comment
Hide comment
@vchakoshy

vchakoshy Jun 16, 2016

Good job. ;)

vchakoshy commented Jun 16, 2016

Good job. ;)

@boulate

This comment has been minimized.

Show comment
Hide comment
@boulate

boulate 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 ? :)

boulate 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.

Show comment
Hide comment
@ASAPHAANING

ASAPHAANING 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"

ASAPHAANING 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.

Show comment
Hide comment
@Monkybusiness

Monkybusiness 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.

Monkybusiness 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.

Show comment
Hide comment
@hadrins

hadrins 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.

hadrins 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.

Show comment
Hide comment
@shurak

shurak Jan 21, 2018

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

shurak commented Jan 21, 2018

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

@GerhardK90

This comment has been minimized.

Show comment
Hide comment
@GerhardK90

GerhardK90 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.

GerhardK90 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.

Show comment
Hide comment
@sebastiaanfranken

sebastiaanfranken 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

sebastiaanfranken 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.

Show comment
Hide comment
@nebbian

nebbian 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?

nebbian 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.

Show comment
Hide comment
@tevkar

tevkar 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.

tevkar 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.

Show comment
Hide comment
@mrmainnet

mrmainnet May 24, 2018

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

mrmainnet 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.

Show comment
Hide comment
@churusaa

churusaa 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

churusaa 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment