#! /bin/bash # Brioche Backup # Full and Incremental backup script with tar and LVM snapshots. # # Copyright (C) 2008, 2009 Amand Tihon # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Note: This script relies on GNU tar specific options, # do not attempt to use it as-is with other tar implementations. # Mandatory CONFIG_FILE="/etc/brioche.conf" ####################################################################### # Default config values. Do not change, edit $CONFIG_FILE ####################################################################### BACKUPTAB="/etc/briochetab" MAILTO="root" REPODIR="/backup" TAR_OPTS="--one-file-system -S " COMPRESS="gz" COMPRESS_OPT="--gzip" SNAPSHOT_MOUNTPOINT="/mnt/backup-snapshot" SNAPSHOT_NAME="backup-snap" SNAPSHOT_SIZE="5G" USAGE_WARN="80" USE_FTP="no" FTP_HOST="ftpback.example.com" FTP_DIR="/" FTP_KEEP="4" # Ensure that we have a minimal PATH PATH=/sbin:/bin:/usr/sbin:/usr/bin FINAL_STATUS="SUCCESS" SUMMARY="/tmp/backup.sumary" ####################################################################### # Helpers ####################################################################### trap 'interrupted ctrl-C' INT trap 'interrupted KILL' TERM NOW() { echo -n `date "+%Y-%m-%d %H:%M:%S"` } # Log a line with timestamp. log() { echo "[`NOW`] $@" } # The summary that will be sent by email summary() { log $@ echo $@ >> $SUMMARY } # Set final status to CRITICAL set_critical() { FINAL_STATUS="CRITICAL" } set_error() { if [ "$FINAL_STATUS" != "CRITICAL" ]; then FINAL_STATUS="ERROR" fi } set_warning() { if [ "$FINAL_STATUS" == "SUCCESS" ]; then FINAL_STATUS="WARNING" fi } finish() { # Check free space local usage=`df -hP "${REPODIR}" | tail -n 1 | tr -s [:space:] | cut -d" " -f5 | tr -d '%'` log "${REPODIR} usage after backup: ${usage}%." if [ "$usage" -ge "$USAGE_WARN" ]; then set_warning summary "Warning : Filesystem ${REPODIR} is ${usage}% full." fi summary "Backup procedure ended on `NOW`" SUBJECT="Backup report for `hostname` (${FINAL_STATUS})." mail -s "${SUBJECT}" "${MAILTO}" < "$SUMMARY" rm -f "$SUMMARY" exit } interrupted() { FINAL_STATUS="INTERRUPTED" summary "Backup procedure interrupted by user ($1)." summary "Take care, an LVM snapshot may still be present !" finish } ####################################################################### # Snapshots ####################################################################### # Make a snapshot of a logical volume. # Usage: make_snapshot vg lv make_snapshot() { log "Creating a snapshot volume of /dev/$1/$2" lvcreate --snapshot -L ${SNAPSHOT_SIZE} -n ${SNAPSHOT_NAME} /dev/$1/$2 RETVAL=$? if [ "$RETVAL" != "0" ]; then log "Error ${RETVAL}: Unable to create a snapshot of /dev/$1/$2" return 1 fi } # Mount the snapshot volume. # On error, tries to remove the snapshot. # Usage: mount_snapshot vg mount_snapshot() { log "Mounting the snapshot volume ${SNAPSHOT_NAME}." if [ ! -d "${SNAPSHOT_MOUNTPOINT}" ]; then log "Creating mountpoint ${SNAPSHOT_MOUNTPOINT}." mkdir -p "${SNAPSHOT_MOUNTPOINT}" fi mount /dev/$1/${SNAPSHOT_NAME} ${SNAPSHOT_MOUNTPOINT} RETVAL=$? if [ "$RETVAL" != "0" ]; then log "Error ${RETVAL}: Unable to mount /dev/$1/${SNAPSHOT_NAME} on ${SNAPSHOT_MOUNTPOINT}" remove_snapshot $1 || return 100 return 1 fi } # Remove a previously created snapshot. It must be unmounted. # Usage: remove_snapshot vg remove_snapshot() { log "Removing the snapshot volume /dev/$1/${SNAPSHOT_NAME}." lvremove -f /dev/$1/${SNAPSHOT_NAME} RETVAL=$? if [ "$RETVAL" != "0" ]; then log "Error ${RETVAL}: Unable to remove the snapshot volume /dev/$1/${SNAPSHOT_NAME}" return 100 fi } # Unmount the previously mounted snapshot. # Usage: unmount_snapshot vg unmount_snapshot() { log "Unmounting the snapshot volume /dev/$1/${SNAPSHOT_NAME}." umount /dev/$1/${SNAPSHOT_NAME} RETVAL=$? if [ "$RETVAL" != "0" ]; then log "Error ${RETVAL}: Unable to unmount the snapshot volume /dev/$1/${SNAPSHOT_NAME}" return 100 fi } ####################################################################### # Main backup functions ####################################################################### # Make a full backup of a directory, with snar file for subsequent incremental # Usage: make_full_backup source_dir hostname volumename # Returns 0 on success, 1 on error make_full_backup() { log "Making full backup of $2 - ${3}." local destdir="${REPODIR}/$2" local today=`date "+%Y%m%d"` local destfile="${destdir}/${3}.full.${today}.tar.${COMPRESS}" local destsnar="${destdir}/${3}.full.snar" # Move previous run to the "undo" directory if [ ! -d "${destdir}/undo" ]; then log "Creating undo/ directory." mkdir -p "${destdir}/undo" fi log "Moving old run into undo/ directory." mv ${destdir}/${3}.* ${destdir}/undo # Do the actual backup. Destination file name and snar are like # /backup/valeron/root.full.20090105.tar.bz2 # /backup/valeron/root.full.snar log "Running tar..." tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1 if [ "$?" = "0" ]; then log "Removing undo/ directory." rm -rf "${destdir}/undo" else log "Error $?: Could not archive $2 - $3" return 1 fi return 0 } # Make an incremental backup of a directory, from full's snar file # Usage: make_incr_backup source_dir hostname volumename # Returns 0 on success, 1 on error, 2 if no previous full is found. make_incr_backup() { log "Making incremental backup of $2 - ${3}." local destdir="${REPODIR}/$2" local today=`date "+%Y%m%d"` local destfile="${destdir}/${3}.incr.${today}.tar.${COMPRESS}" local destsnar="${destdir}/${3}.incr.${today}.snar" local fullsnar="${destdir}/${3}.full.snar" # Test existence of full backup if [ ! -e $fullsnar ]; then log "Could not find catalog ${fullsnar}." return 2 fi # Prepare the copy of the snar file cp "$fullsnar" "$destsnar" # Do the actual backup. Destination file name and snar are like # /backup/valeron/root.incr.20090105.tar.bz2 # /backup/valeron/root.incr.20090105.snar log "Running tar..." tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1 if [ ! "$?" = "0" ]; then log "Error $?: Could not archive $2 - $3" return 1 fi return 0 } ####################################################################### # FTP functions ####################################################################### # Push everything in the directory given in $1 to the FTP server, under # /FTP_DIR/$2/latest ftp_push() { log "Mirror $1 on FTP (${FTP_HOST})." local source="${1}" local target="${FTP_DIR}/${2}/latest" local command="mkdir -p ${target}; cd ${target}" command="${command}; mirror --reverse --only-newer --verbose ${source}" lftp -e "${command}; exit" ${FTP_HOST} } # Rotate old files on FTP # Usage: ftp_rotate group ftp_rotate() { log "Rotating backups of $1 on FTP (${FTP_HOST})." local lastrun="run-${FTP_KEEP}" local target="${FTP_DIR}/${1}" local commands="mkdir -p ${target}; cd ${target}" # Build commands # Remove oldest run if [ "$FTP_KEEP" != "0" ]; then commands="$commands; rm -rf ${lastrun}" # Move everything back for run in `seq $FTP_KEEP -1 2`; do local newer=$run let "newer -= 1" commands="$commands; mv run-$newer run-$run" done # Move "old latest" to run-1 commands="$commands; mv latest run-1" else commands="$commands; rm -rf latest" fi # Create "new latest" directory commands="$commands; mkdir latest; exit" # Run the commands on the FTP server lftp -e "$commands" $FTP_HOST } ####################################################################### # Start here ####################################################################### # Truncate summary file, start header blurb echo "" > $SUMMARY summary "Backup procedure started on `NOW`" if [ -r "${CONFIG_FILE}" ] then source "${CONFIG_FILE}" else summary "Error: Unable to read configuration file ${CONFIG_FILE}. Aborting." set_critical finish fi if [ ! -r "${BACKUPTAB}" ] then summary "Error: Unable to read ${BACKUPTAB}. Aborting." set_critical finish fi # TODO: Something cleaner, here... DO_FULL_BACKUP="no" case "$1" in --full|-f) DO_FULL_BACKUP="yes" ;; --help|-h) echo "Usage: ${0} [-f]" exit 0 ;; esac # Discover which COMPRESS_OPT to use, from COMPRESS case "$COMPRESS" in gz) COMPRESS_OPT="--gzip" ;; bz2) COMPRESS_OPT="--bzip2" ;; lzma) COMPRESS_OPT="--lzma" ;; none) COMPRESS_OPT="" COMPRESS="" ;; *) summary "Unknown compression method: ${COMPRESS}. Falling back to gzip." COMPRESS="gz" COMPRESS_OPT="--gzip" ;; esac ####################################################################### # Parse backuptab file, call backup functions for each line ####################################################################### # Rotate FTP groups on full if [ "$USE_FTP" = "yes" -a "$DO_FULL_BACKUP" = "yes" ]; then for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u` do ftp_rotate $group done fi # Ignore empty and commented lines grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | tr -s [:space:]| while read line do # split line in fields device=`echo $line|cut -d" " -f1` dosnap=`echo $line|cut -d" " -f2` group=`echo $line|cut -d" " -f3` volume=`echo $line|cut -d" " -f4` # Make and mount snapshot if needed. if [ "$dosnap" = "yes" ]; then # Split the device to find the VG and LV vg=`echo $device|cut -d"/" -f3` lv=`echo $device|cut -d"/" -f4` make_snapshot $vg $lv if [ "$?" != "0" ]; then summary "Could not take a snapshot of $device" set_error continue # Next one fi mount_snapshot $vg RETVAL="$?" if [ "$RETVAL" != "0" ]; then summary "Could not mount the snapshot of $device" set_error if [ "$RETVAL" = "100" ]; then summary "Could not remove the snapshot !" set_critical finish fi continue # Next one fi BACKUP_SOURCE="${SNAPSHOT_MOUNTPOINT}" else BACKUP_SOURCE="$device" fi # Make the backup if [ "$DO_FULL_BACKUP" = "no" ]; then make_incr_backup ${BACKUP_SOURCE} $group $volume RETVAL=$? if [ "$RETVAL" = "0" ]; then summary "INCREMENTAL backup of $device done on `NOW`." elif [ "$RETVAL" = "1" ]; then summary "Error during incremental backup of $device" set_error elif [ "$RETVAL" = "2" ]; then summary "Can't do an incremental backup without a full one being present." summary "Switching to full backup for $device" make_full_backup ${BACKUP_SOURCE} $group $volume if [ "$?" = "0" ]; then summary "FULL backup of $device done on `NOW`." else summary "Error during full backup of $device" set_error fi else summary "Unknown error during incremental backup of $device" set_error fi else # Do a full backup make_full_backup ${BACKUP_SOURCE} $group $volume if [ "$?" = "0" ]; then summary "FULL backup of $device done on `NOW`." else summary "Error during full backup of $device" set_error fi fi # Time to unmount and destroy the snapshot, if needed. # Any error here is critical, since the snapshot and the mountpoint are # the same for each device... if [ "$dosnap" = "yes" ]; then unmount_snapshot $vg if [ "$?" != "0" ]; then summary "Could not unmount snapshot !" set_critical finish fi remove_snapshot $vg if [ "$?" != "0" ]; then summary "Could not destroy the snapshot !" set_critical finish fi fi done # Push everything on the FTP if [ "$USE_FTP" = "yes" ]; then summary "" for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u` do ftp_push "$REPODIR/$group" $group summary "Mirrored ${group} to ${FTP_HOST}." done fi finish