|
#!/bin/bash |
|
# A simple backup script for kvm |
|
# Original source: http://soliton74.blogspot.no/2013/08/about-kvm-qcow2-live-backup.html |
|
# |
|
# Author: Luca Lazzeroni <[email protected]> |
|
# Web: http://soliton74.blogspot.it |
|
# |
|
# Patched by: Tim Miller Dyck |
|
# |
|
# Patched by: Runar Ingebrigtsen <[email protected]> |
|
# Web: http://rin.no |
|
# |
|
# Patched by Jack Rutheford <[email protected]> |
|
# https://gist.github.com/withakay/4dfbc18d16dc699cee4be4b55539c400 |
|
# |
|
#################################################### |
|
# This script is shared as-is. Do whatever you want. |
|
#################################################### |
|
|
|
# Default base backup path -- this directory will be created if it does not already exist |
|
BKPATH="/bak/libvirt" |
|
|
|
# String to match in list of VM block devices |
|
VMDATAMATCH="qcow2" |
|
|
|
# Maximum history of VM backups allowed (e.g. 2 means keep the current backup and one previous backup) |
|
MAXBACKUP=2 |
|
|
|
# Default email (an empty string means no e-mail will be sent) |
|
TARGET_EMAIL="" |
|
|
|
# Default sender email |
|
SENDER_NAME="VM Backup" |
|
SENDER_EMAIL="" |
|
|
|
######################################## |
|
|
|
LOGLINES="" |
|
|
|
# logging function |
|
|
|
function logLineLocal() { |
|
ORA=$(date +"%D %H:%M:%S") |
|
echo "$ORA - $@" |
|
} |
|
|
|
function logLine() { |
|
ORA=$(date +"%D %H:%M:%S") |
|
echo "$ORA - $@" |
|
LOGLINES+="$ORA - $@\n" |
|
} |
|
|
|
# if needed, send log via mail |
|
|
|
function doExit() { |
|
# Compose message and send it |
|
if [ "x$TARGET_EMAIL" != "x" ]; then |
|
local MESSAGE="To: $TARGET_EMAIL\nSubject: " |
|
if [ $1 -eq 0 ]; then |
|
MESSAGE+="Backup ##OK##\n\n$LOGLINES\n" |
|
else |
|
MESSAGE+="Backup ##FAILED##\n\n$LOGLINES\n" |
|
fi |
|
echo -e $MESSAGE | sendmail -f$SENDER_EMAIL -F"$SENDER_NAME" $TARGET_EMAIL |
|
fi |
|
exit $1 |
|
} |
|
|
|
# get domain info |
|
|
|
function checkDomain() { |
|
local VMNAME=$1 |
|
DOMINFO=$(virsh dominfo $VMNAME 2> /dev/null) |
|
if [ "x$DOMINFO" == "x" ]; then |
|
logLine "Cannot find domain $VMNAME" |
|
doExit 1 |
|
fi |
|
} |
|
|
|
# get vm devices |
|
|
|
function getVMBlockDevs() { |
|
local VMNAME=$1 |
|
# Filter list of all block devices by VMDATAMATCH, and strip string to full path |
|
BLOCKDEVS=$(virsh domblklist $VMNAME 2> /dev/null | grep '.img$\|.qcow2$' | sed -e 's/.* //') |
|
if [ "x$BLOCKDEVS" == "x" ]; then |
|
logLine "Cannot get block device list for domain. Exit." |
|
doExit 1 |
|
fi |
|
# Get the format of the virtual drive |
|
FMTS=$(echo $BLOCKDEVS | xargs -n 1 qemu-img info | grep 'file format:' | sed -e 's/file format: //g') |
|
# We only want qcow2, perform a replace, the result should be empty |
|
QCOW2=$(echo $FMTS | sed 's/qcow2\|qcow2\s//g') |
|
# If we have anything other than an empty string then error |
|
if [ ! "x$QCOW2" == "x" ]; then |
|
logLine "Block devices are not qcow2 ($QCOW2). Exit." |
|
doExit 1 |
|
fi |
|
|
|
} |
|
|
|
# abort block copy job |
|
function abortCopy() { |
|
logLine "Cancel job for disk $2 on vm $1" |
|
virsh blockjob $1 $2 --abort 2> /dev/null |
|
} |
|
|
|
# abort all block copy jobs |
|
function abortAllJobs() { |
|
logLine "Cancelling all block-copy jobs" |
|
for i in $BLOCKDEVS; do |
|
abortCopy $1 $i 2> /dev/null |
|
done |
|
} |
|
|
|
# save vm state (RAM) - this will power off the VM |
|
function saveVMState() { |
|
local VMNAME=$1 |
|
logLine "Saving VM memory for $VMNAME" |
|
logLine $(virsh save $VMNAME $BKPATH/$VMNAME-memory --running 2>&1) |
|
} |
|
|
|
# dump XML domain definition |
|
function dumpVMdefinition() { |
|
local VMNAME=$1 |
|
local DUMPNAME="$BKPATH/$VMNAME.xml" |
|
logLine "Dumping definition of $VMNAME to $DUMPNAME" |
|
virsh dumpxml --security-info $VMNAME > $DUMPNAME 2> /dev/null |
|
if [ $? -eq 1 ]; then |
|
logLine "VM $1 doesn't exists" |
|
doExit 1 |
|
fi |
|
} |
|
|
|
# Make the VM transient |
|
function undefineVM() { |
|
local VMNAME=$1 |
|
logLine "Destroying vm definition for VM $VMNAME" |
|
logLine $(virsh undefine $VMNAME) |
|
} |
|
|
|
# Suspend the VM |
|
function suspendVM() { |
|
local VMNAME=$1 |
|
logLine "Suspending vm $VMNAME" |
|
logLine $(virsh suspend $VMNAME) |
|
} |
|
|
|
# restore VM state. this is necessary because the save-vm stop the vm |
|
function restoreVMState() { |
|
local VMNAME=$1 |
|
logLine "Restoring vm $VMNAME" |
|
logLine $(virsh restore $BKPATH/$VMNAME-memory --running) |
|
} |
|
|
|
# restore VM definition |
|
function defineVM() { |
|
local VMNAME=$1 |
|
logLine "Restoring VM definition for vm $VMNAME" |
|
logLine $(virsh define $BKPATH/$VMNAME.xml) |
|
} |
|
|
|
function safeExit() { |
|
# exit recoverying vm |
|
local VMNAME=$1 |
|
local MESSAGE=$2 |
|
logLine $MESSAGE |
|
# abort all jobs |
|
abortAllJobs $VMNAME |
|
# re-define domain |
|
defineVM $VMNAME |
|
# exit |
|
logLine "Safe exit done." |
|
doExit 1 |
|
} |
|
|
|
# start the blockcopy job |
|
function copyBlock() { |
|
local VMNAME=$1 |
|
local DISKNAME=$2 |
|
local BAKDISKNAME=$(basename $2) |
|
local BAKNAME="$BKPATH/$BAKDISKNAME" |
|
logLine "Copying disk $DISKNAME for vm $VMNAME into file $BAKNAME..." |
|
virsh blockcopy $VMNAME $DISKNAME $BAKNAME 2> /dev/null |
|
if [ $? -gt 0 ]; then |
|
safeExit $VMNAME "Problem starting blockcopy" |
|
fi |
|
local PROGRESS=0 |
|
until [ $PROGRESS -eq 100 ]; do |
|
PROGRESS=$(virsh blockjob $VMNAME $DISKNAME 2>&1 | egrep -o "([0-9]{1,3})") |
|
if [ "x$PROGRESS" == "x" ]; then |
|
safeExit $VMNAME "BlockJob aborted. Disk full ?" |
|
fi |
|
logLineLocal "Copying... $PROGRESS %" |
|
sleep 5; |
|
done |
|
} |
|
|
|
function getBackupName() { |
|
# compose the backup name |
|
local VMNAME=$1 |
|
local BACKUP_IDX=$2 |
|
echo "$BKPATH/$VMNAME/backup-$BACKUP_IDX" |
|
} |
|
|
|
function getLastBackupName() { |
|
local VMNAME=$1 |
|
local VMBACKUP_IDX=$((MAXBACKUP-1)) |
|
getBackupName "$VMNAME" $VMBACKUP_IDX |
|
} |
|
|
|
function fixPath() { |
|
# get real backup path for a vm |
|
local VMNAME=$1 |
|
local BASEPATH="$BKPATH/$VMNAME" |
|
local OLDER_BACKUP_PATH=$(getBackupName $VMNAME $MAXBACKUP) |
|
# rotate backup |
|
let BKCNT=$((MAXBACKUP-2)) |
|
while [ $BKCNT -ge 0 ]; do |
|
let PREVBKCNT=$((BKCNT+1)) |
|
local BKPREV=$(getBackupName $VMNAME $PREVBKCNT) |
|
local BK=$(getBackupName $VMNAME $BKCNT) |
|
logLine "Check for move $BK => $BKPREV" |
|
if [ -d "$BKPREV" ] && [ -d "$BK" ]; then |
|
logLine "Remove old backup $BKPREV" |
|
# safety measure |
|
mv $BKPREV "$BKPATH/$VMNAME/to-be-removed" |
|
if [ -f "$BKPATH/$VMNAME/to-be-removed/$VMNAME.xml" ]; then |
|
logLine "Safely remove old backup directory" |
|
rm -rf "$BKPATH/$VMNAME/to-be-removed" |
|
else |
|
logLine "Cannot remove old backup directory. Not a backup." |
|
doExit 1 |
|
fi |
|
fi |
|
if [ -d "$BK" ]; then |
|
logLine "Rename $BK to $BKPREV" |
|
mv $BK $BKPREV |
|
fi |
|
let BKCNT-=1 |
|
done |
|
# fix the global-path |
|
BKPATH=$(getBackupName $VMNAME 0) |
|
# create the backup directory |
|
mkdir -p "$BKPATH" |
|
if [ $? -gt 0 ]; then |
|
logLine "Problem creating backup path. Exiting." |
|
doExit 1 |
|
fi |
|
# fix permissions |
|
chown -R libvirt-qemu:kvm "$BASEPATH" |
|
} |
|
|
|
function checkDiskSpace() { |
|
local VMNAME=$1 |
|
logLine "Checking required disk space" |
|
let VMREQSPACE=0 |
|
# get free space on volume |
|
local DSPACE=$(df "$BKPATH" | grep -v "Available" | awk '{ print $4 }') |
|
if [ "x$DSPACE" == "x" ]; then |
|
logFile "Unable to detect disk space for vm" |
|
doExit 1 |
|
fi |
|
# for each blockdev get space |
|
for i in $BLOCKDEVS; do |
|
local BLKSPACE=$(virsh domblkinfo $VMNAME $i | grep "Physical:" | awk '{ print $2 }') |
|
local KILOBLKSPACE=$((BLKSPACE / 1024)) |
|
logLine "Device $i requires $KILOBLKSPACE Kbytes" |
|
let VMREQSPACE+=$KILOBLKSPACE |
|
done |
|
# get memory size (for calculating disk memory needed) |
|
MEMSIZE=$(virsh dominfo $VMNAME | grep "Max memory" | awk '{ print $3 }') |
|
if [ "x$MEMSIZE" == "x" ]; then |
|
logLine "Cannot find VM required memory size" |
|
doExit 1 |
|
fi |
|
logLine "VM memory size is $MEMSIZE Kbytes" |
|
local VMROUNDMEMSIZE=$(awk "BEGIN{ print int($MEMSIZE*1.2) }") |
|
logLine "VM memory requirement scaled to $VMROUNDMEMSIZE Kbytes" |
|
# Add extra 4Gb for memory and xml |
|
let VMREQSPACE+=$VMROUNDMEMSIZE |
|
logLine "Total space required by backup is $VMREQSPACE Kilobytes" |
|
# get space used by last backup (which will be thrown away) |
|
local LASTBACKUPNAME=$(getLastBackupName $VMNAME) |
|
local LASTBACKUPSPACE=$(du -s $LASTBACKUPNAME 2> /dev/null | awk '{ print $1 }') |
|
if [ "x$LASTBACKUPSPACE" == "x" ]; then |
|
LASTBACKUPSPACE=0 |
|
fi |
|
logLine "Last backup $LASTBACKUPNAME occupies $LASTBACKUPSPACE Kbytes" |
|
# subtract the last backup space from VMREQSPACE |
|
let VMREQSPACE-=$((LASTBACKUPSPACE)) |
|
# now get available space on device |
|
local MBSPACEFREE=$((DSPACE / 1024)) |
|
local MBSPACEREQUIRED=$((VMREQSPACE / 1024)) |
|
if [ $DSPACE -lt $VMREQSPACE ]; then |
|
logLine "Cannot make backup; only ${MBSPACEFREE}Mb avaliable. Minimum needed space is ${MBSPACEREQUIRED}Mb." |
|
doExit 1 |
|
else |
|
logLine "Backup possibile: ${MBSPACEFREE}Mb availables vs ${MBSPACEREQUIRED}Mb required." |
|
fi |
|
} |
|
|
|
function backupVM() { |
|
local VMNAME=$1 |
|
|
|
# create the base backup directory if it does not exist and set correct ownership |
|
mkdir -p "$BKPATH" |
|
if [ $? -gt 0 ]; then |
|
logLine "Problem creating base backup directory. Exiting." |
|
doExit 1 |
|
fi |
|
|
|
# set permissions |
|
chown -R libvirt-qemu:kvm "$BKPATH" |
|
|
|
# start backup |
|
logLine "Backup of $VMNAME started" |
|
|
|
# get vm block devices |
|
getVMBlockDevs $VMNAME |
|
|
|
# check disk space |
|
checkDiskSpace $VMNAME |
|
|
|
# fix/rotate path |
|
fixPath $VMNAME |
|
# dump the vm definition |
|
dumpVMdefinition $VMNAME |
|
# make it transient |
|
undefineVM $VMNAME |
|
# copy all block devices |
|
for i in $BLOCKDEVS; do |
|
copyBlock $VMNAME $i |
|
done |
|
|
|
# suspend vm (needed to suspend I/O and abort jobs) |
|
suspendVM $VMNAME |
|
# abort jobs |
|
abortAllJobs $VMNAME |
|
# save vm state and power it off |
|
saveVMState $VMNAME |
|
# restore VM state. this is necessary because the save-vm stop the vm |
|
restoreVMState $VMNAME |
|
# restore vm definition |
|
defineVM $VMNAME |
|
|
|
# end backup |
|
logLine "Backup of $VMNAME finished" |
|
} |
|
|
|
function show_help() { |
|
echo "kvm-backup [options] domainname" |
|
echo -e "\nwith [options] assuming following values:" |
|
echo "-h or -? show this help" |
|
echo "-t DIR set target directory for the backup (default is $BKPATH)" |
|
echo "-m [1-9] set number of backups to keep (default is $MAXBACKUP)" |
|
echo "-e EMAIL send log via mail to address (no default)" |
|
} |
|
|
|
|
|
# |
|
# MAIN CODE |
|
# |
|
|
|
while getopts "h?t:m:e:" opt; do |
|
case "$opt" in |
|
h|\?) |
|
show_help |
|
exit 0 |
|
;; |
|
t) |
|
logLine "Target set to $OPTARG" |
|
BKPATH=$OPTARG |
|
;; |
|
m) |
|
if [ "x$OPTARG" == "x" ] || [ $OPTARG -le 0 ] || [ $OPTARG -gt 9 ]; then |
|
logLine "Invalid number of backups to keep specified. It must be between 1 and 9." |
|
exit 1 |
|
fi |
|
logLine "Number of backups to keep set to $OPTARG" |
|
MAXBACKUP=$OPTARG |
|
;; |
|
e) |
|
logLine "Sending email to $OPTARG" |
|
TARGET_EMAIL=$OPTARG |
|
;; |
|
esac |
|
done |
|
shift $((OPTIND-1)) |
|
|
|
# Parse remaining options |
|
|
|
if [ "$1" == "" ]; then |
|
logLine "Missing VM name" |
|
exit 1; |
|
fi |
|
|
|
# go |
|
VM=$1 |
|
|
|
# check if vm exists |
|
checkDomain $VM |
|
|
|
# finally do the backup |
|
backupVM $VM |
|
|
|
# exit and eventually send logs |
|
doExit 0 |