Backup KVM virtual machines

/ virtualization, linux

Purpose of this script: Live Backup of KVM Virtual Machines on QCOW2 images

This script was developed and tested on Centos 8 OS.

Dont Run this script on production server! First run it in a test environment.

Instruction

  1. Save this scipt to backup-vm.sh file and add run permission:
chmod +x backup-vm.sh
  1. Set script variables:
#!/bin/bash

### SETTINGS
# backup dir
BACKUP_DIR=/virt/backup
# last full backup images count
FULL_IMAGES_COUNT=2
# last snapshots count
SNAPSHOT_COUNT=4

### OTHER VARIABLES
# timestamp
TIMESTAMP=`date +%Y%m%d-%H%M%S`
# target vm list (running)
VM_LIST=`virsh list | grep running | awk '{print $2}'`
# log file
LOGFILE="/var/log/kvmbackup.log"

# write log
echo -e "\n**********\n`date`: START BACKUP OF HOST: `hostname`\n"
echo -e "\n**********\n`date`: START BACKUP OF HOST: `hostname`\n" >> $LOGFILE
echo -e "Running VM's:\n`virsh list | grep running | awk '{print "*",$2}'`"
echo -e "Running VM's:\n`virsh list | grep running | awk '{print "*",$2}'`" >> $LOGFILE

for ACTIVEVM in $VM_LIST
    do
        # create directories
        mkdir -p $BACKUP_DIR/$ACTIVEVM/tmp-ext-snap
        mkdir -p $BACKUP_DIR/$ACTIVEVM/config
        mkdir -p $BACKUP_DIR/$ACTIVEVM/image
        # add vm disk names to array
        DISK_ARR=(`virsh domblklist $ACTIVEVM | grep -e vd -e sd | grep -e '/' | awk '{print $1}'`)
        # list vm disk names
        DISK_NAMES=`virsh domblklist $ACTIVEVM | grep -e vd -e sd | grep -e '/' | awk '{print $1}'`
        # list vm disk paths
        DISK_PATH=`virsh domblklist $ACTIVEVM | grep -e vd -e sd | grep -e '/' | awk '{print $2}'`

        ### VM CONFIG BACKUP
        # write log
        echo -e "\n--- `date`: Start VM backup of: $ACTIVEVM\n"
        echo -e "\n--- `date`: Start VM backup of: $ACTIVEVM\n" >> $LOGFILE
        virsh dumpxml $ACTIVEVM > $BACKUP_DIR/$ACTIVEVM/config/$ACTIVEVM-$TIMESTAMP.xml

        ### CREATE SNAPSHOT
        # write log
        echo "`date`: Create VM snapshot of: $ACTIVEVM"
        echo "`date`: Create VM snapshot of: $ACTIVEVM" >> $LOGFILE
        virsh snapshot-create-as --domain $ACTIVEVM --name snapshot-$TIMESTAMP
        sleep 2

        ### CREATE TEMPORARY EXTERNAL SNAPSHOT
        # write log
        echo "`date`: Create temporary external VM snapshot of: $ACTIVEVM"
        echo "`date`: Create temporary external VM snapshot of: $ACTIVEVM" >> $LOGFILE
        for DISK_ARR_ITEM in ${!DISK_ARR[*]}
            do
                DISKSPEC_ARR+=("--diskspec ${DISK_ARR[$DISK_ARR_ITEM]},file=$BACKUP_DIR/$ACTIVEVM/tmp-ext-snap/snapshot-`echo "${DISK_ARR[$DISK_ARR_ITEM]}"`-$TIMESTAMP.qcow2,snapshot=external")
            done
        virsh snapshot-create-as \
            --domain $ACTIVEVM tmp-ext-snap-$TIMESTAMP ${DISKSPEC_ARR[*]} \
            --disk-only \
            --atomic
        # empty array
        unset DISKSPEC_ARR
        sleep 2

        ### VM DISK BACKUP
        for DISK_PATH_ITEM in $DISK_PATH
            do
                # write log
                echo "`date`: Create VM backup of: $ACTIVEVM [$DISK_PATH_ITEM]"
                echo "`date`: Create VM backup of: $ACTIVEVM [$DISK_PATH_ITEM]" >> $LOGFILE
                # get filename from path
                FILENAME=`basename $DISK_PATH_ITEM`
                # save and shrink image
                mkdir -p $BACKUP_DIR/$ACTIVEVM/image/$FILENAME
                qemu-img convert -O qcow2 -c $DISK_PATH_ITEM $BACKUP_DIR/$ACTIVEVM/image/$FILENAME/$FILENAME.$TIMESTAMP.bak
                sleep 2
            done

        ### COMMIT SNAPSHOT
        for DISK_NAMES_ITEM in $DISK_NAMES
            do
                # get path of snapshot
                SNAP_PATH=`virsh domblklist $ACTIVEVM | grep $DISK_NAMES_ITEM | awk '{print $2}'`
                # write log
                echo "`date`: Commit snapshot: $ACTIVEVM [$SNAP_PATH]"
                echo "`date`: Commit snapshot: $ACTIVEVM [$SNAP_PATH]" >> $LOGFILE
                # commit snapshot
                virsh blockcommit $ACTIVEVM $DISK_NAMES_ITEM --active --verbose --pivot
                sleep 2
            done

        ### DELETE TEMPORARY EXTERNAL SNAPSHOTS (VM)
        SNAPSHOT_LIST=`virsh snapshot-list $ACTIVEVM | grep tmp-ext-snap | awk '{print $1}'`
        # write log
        echo "`date`: Delete temporary external snapshot (vm) of: $ACTIVEVM [$SNAPSHOT_LIST]"
        echo "`date`: Delete temporary external snapshot (vm) of: $ACTIVEVM [$SNAPSHOT_LIST]" >> $LOGFILE
        for SNAPSHOT_LIST_ITEM in $SNAPSHOT_LIST
            do
                virsh snapshot-delete $ACTIVEVM $SNAPSHOT_LIST_ITEM --metadata
            done

        ### DELETE TEMPORARY EXTERNAL SNAPSHOTS (FILE)
        # write log
        echo -e "`date`: Delete temporary external snapshot (file) of: $ACTIVEVM"
        echo -e "`date`: Delete temporary external snapshot (file) of: $ACTIVEVM" >> $LOGFILE
        echo -e "`find $BACKUP_DIR/$ACTIVEVM/tmp-ext-snap/* -exec ls -ltrh {} + | awk '{print $9}'`\n"
        echo -e "`find $BACKUP_DIR/$ACTIVEVM/tmp-ext-snap/* -exec ls -ltrh {} + | awk '{print $9}'`\n" >> $LOGFILE
        find $BACKUP_DIR/$ACTIVEVM/tmp-ext-snap/* -exec rm {} \;

        ### DELETE OLD CONFIG FILES
        # write log
        echo "`date`: Delete old config files of: $ACTIVEVM"
        echo "`date`: Delete old config files of: $ACTIVEVM" >> $LOGFILE
        CONFIG_ARR=(`ls $BACKUP_DIR/$ACTIVEVM/config/* -1tr`)
            if (( ${#CONFIG_ARR[*]} > $FULL_IMAGES_COUNT ))
                then
                    echo "* ${CONFIG_ARR[0]} - deleted"
                    rm ${CONFIG_ARR[0]}
                fi

        ### DELETE OLD IMAGES
        # write log
        echo "`date`: Delete old full images of: $ACTIVEVM"
        echo "`date`: Delete old full images of: $ACTIVEVM" >> $LOGFILE
        for DISK_PATH_ITEM in $DISK_PATH
            do
                FILENAME=`basename $DISK_PATH_ITEM`
                IMAGE_ARR=(`ls $BACKUP_DIR/$ACTIVEVM/image/$FILENAME/* -1tr`)
                if (( ${#IMAGE_ARR[*]} > $FULL_IMAGES_COUNT ))
                    then
                        echo "* ${IMAGE_ARR[0]} - deleted"
                        rm ${IMAGE_ARR[0]}
                fi
            done

        ### DELETE OLD SNAPSHOTS
        # write log
        echo "`date`: Delete old snapshots of: $ACTIVEVM"
        echo "`date`: Delete old snapshots of: $ACTIVEVM" >> $LOGFILE
        SNAPSHOT_ARR=(`virsh snapshot-list $ACTIVEVM | grep snapshot | awk '{print $1}'`)
            if (( ${#SNAPSHOT_ARR[*]} > $SNAPSHOT_COUNT ))
                then
                    virsh snapshot-delete $ACTIVEVM ${SNAPSHOT_ARR[0]}
                fi

        ### SNAPSHOT LIST
        # write log
        echo -e "Current snapshot list:\n`virsh snapshot-list $ACTIVEVM`"
        echo -e "Current snapshot list:\n`virsh snapshot-list $ACTIVEVM`" >> $LOGFILE

        ### END BACKUP
        # write log
        echo -e "\n`date`: End VM backup of: $ACTIVEVM"
        echo -e "\n`date`: End VM backup of: $ACTIVEVM" >> $LOGFILE
    done

# write log
echo -e "\n`date`: END BACKUP OF HOST: `hostname`\n**********\n"
echo -e "\n`date`: END BACKUP OF HOST: `hostname`\n**********\n" >> $LOGFILE

Update 14 JAN 2021

# fixed issue with snapshot chronological sorting
TIMESTAMP=`date +%d%m%Y-%H%M%S`
# >
TIMESTAMP=`date +%Y%m%d-%H%M%S`

# added compression "-c"
qemu-img convert -O qcow2 $DISK_PATH_ITEM $BACKUP_DIR/$ACTIVEVM/image/$FILENAME/$FILENAME.$TIMESTAMP.bak
# >
qemu-img convert -O qcow2 -c $DISK_PATH_ITEM $BACKUP_DIR/$ACTIVEVM/image/$FILENAME/$FILENAME.$TIMESTAMP.bak

Next Post Previous Post