#!/bin/bash # # backupManager # An engine for processing and managing disk-to-disk backups. # It will create tardumps (gzip compressed if possible) to a specified # target resource such as an NFS server (also works with Windows SFU NFS). # # Tested on Solaris 10 and Linux # # Modified on: September 26th, 2008 # # Written by Justin Mercier # aka The Troubleshooting Ninja # www.troubleshootingninja.com # Released under GPLv2, attribution appreciated but not required. # # Usage: backup [-s | --source] [-r | --repo] [-p | --prefix] \ # [-a | --pre] [-a | --post] [-f | --force] # [source] is file or directory to backup. # if not set it must be defined below. # [repo] is the directory where backup tarzips are stored. # if not set it must be defined below. # [prefix] is the name of the backup file prefix and folder name to be stored in # It will also be appended with a timestamp # i.e. /repo/prefix/prefix_200809201515.tgz # If not set it will be parsed from the source's basename # [pre] is the preprocess command to run before backing up # It should be quoted with "" # [post] is the postprocess command to run after backing up # It should be quoted with "" # [force] specifies whether to override change detection and force a # backup. Values are 0 for no or 1 for yes. # # Example 1: ./backupManager # - will backup the default source to the default repository as # defined by variables in this script below # Example 2: ./backupManager -s=/var/www/html -r=/net/fs01/backups -p=website # - will backup /var/www/html to /net/fs01/backups/website # Example 3: ./backupManager --source=/var/www/html # - will backup /var/www/html to the html directory on the default # repository # Example 4: ./backupManager -s=/usr/local/mysql/data \ # -b="/etc/init.d/mysqld stop" -post="/etc/init.d/mysqld start" # - will stop MySQL, backup its data directory, and then start it. # # die function to echo a message and exit function die ( ) { echo "[FATAL] - $@ ...exiting." exit 1 } # warn function for non-catastrophic errors function warn () { if [ $WARN ] && [ "$WARN" = "ON" ]; then echo "[WARN] - $@." fi } # debug function for considtional debug output function debug () { if [ $DEBUG ] && [ "$DEBUG" = "ON" ]; then echo "[DEBUG] - $@" fi } # info function for conditional verbose output function info () { if [ $VERBOSE ] && [ "$VERBOSE" = "ON" ]; then echo "[INFO] - $@" fi } # info_n and info_tag function for conditional verbose output without newline function info_n () { if [ $VERBOSE ] && [ "$VERBOSE" = "ON" ]; then echo -n "[INFO] - $@" fi } function info_tag () { if [ $VERBOSE ] && [ "$VERBOSE" = "ON" ]; then echo "$@" fi } # function to print out optional annoyance messages function gossip () { if [ $VERBOSE ] && [ $VERBOSE_LVL ] && [ "$VERBOSE" = "ON" ] && [ "$VERBOSE_LVL" = "HIGH" ]; then echo "[INFO] - $@" fi } # function to get the last modification time of a file function get_mtime () { if [ -f $@ ]||[ -d $@ ]; then echo `stat -c %y $@ | awk '{ print $1 $2 }' | sed -e 's/-//g' -e 's/://g' -e 's/............$//'` fi } # function to parse CLAs function parseCLA () { debug "parseCLA called with $@" for i in "$@"; do case $i in -s=*|--source=*) BACKUP_SOURCE_NODE=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; -r=*|--repo=*) BACKUP_TARGET_REPO=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; -p=*|--prefix=*) PREFIX=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; -b=*|--pre=*) PREPROCESS_CMD=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; -a=*|--post=*) POSTPROCESS_CMD=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; -f=*|--force=*) FORCE=`echo $i | sed 's/[-a-zA-Z0-9]*=//'` ;; --help|-?) echo echo "`basename $0` -s=source -r=repo =p=prefix -b=preprocess -a=postprocess" echo echo "[ -s | --source ] is the SOURCE to backup" echo "[ -r | --repo ] is the REPOSITORY to backup to, such as an NFS share" echo "[ -p | --prefix ] is the PREFIX to use in file and folder naming" echo "[ -b | --pre ] is the preprocess to run BEFORE the backup" echo "[ -a | --post ] is the postprocess to run AFTER the backup" echo "[ -f | --force ] circumvent change detection and force a backup" echo "[ -? | --help ] prints this message" echo echo "Example:" echo " # `basename $0` -s=/opt/sybase/data --repo=/net/fileserver/backups \\" echo " -b=\"/etc/init.d/sybase stop\" -post=\"/etc/init.d/sybase start\"" echo exit 0 ;; *) echo "Invalid Option: `echo $i | sed 's/[-a-zA-Z0-9]*=//'`" exit 0 esac done } # Specify the default backup source. Will be overrided by first argument if provided. BACKUP_SOURCE_NODE=/var/www/html # Specify the backup repository. Will be overrided by second argument if provided. BACKUP_TARGET_REPO=/net/fs01/backups # Specify whether to truncate older backups. Values are ON or OFF. TRUNCATE=ON # Specify the number of backups to rotate if TRUNCATE is ON. QUOTA=8 # Specify whether to change permissions (if possible) when finished. YES or NO. CHMOD=YES # Specify octal permission mode if CHMOD is YES CHMOD_MODE=777 # Administrative variables. Probably best not to change these. LOCKFILE=/tmp/`basename "$0"`.lock WARN=ON DEBUG=OFF VERBOSE=ON VERBOSE_LVL=LOW # call CLA parser parseCLA "$@" if [ "$BACKUP_SOURCE_NODE" ]; then debug "BSN - $BACKUP_SOURCE_NODE"; fi if [ "$BACKUP_TARGET_REPO" ]; then debug "BTR - $BACKUP_TARGET_REPO"; fi if [ "$PREFIX" ]; then debug "PREFIX - $PREFIX"; fi if [ "$PREPROCESS_CMD" ]; then debug "pre - $PREPROCESS_CMD"; fi if [ "$POSTPROCESS_CMD" ]; then debug "post - $POSTPROCESS_CMD"; fi #### MAIN #### info "Running `basename \"$0\"` at `date`." debug "Defaults: $BACKUP_SOURCE_NODE $BACKUP_TARGET_REPO" # check to see if we're already running [ -f $LOCKFILE ] && die "lockfile $LOCKFILE exists" # create a lock and trap exit status for removal trap "{ rm -f $LOCKFILE; exit 255; }" 2 trap "{ rm -f $LOCKFILE; exit 255; }" 9 trap "{ rm -f $LOCKFILE; exit 255; }" 15 trap "{ rm -f $LOCKFILE; exit 0; }" EXIT touch $LOCKFILE # Test the settings passed as args or default values if [ ! $PREFIX ]; then PREFIX=`basename $BACKUP_SOURCE_NODE` && debug "No CLA for prefix, divining PREFIX=$PREFIX" fi if [ ! -e "$BACKUP_SOURCE_NODE" ]; then die "Source $BACKUP_SOURCE_NODE does not exist" else if [ ! -d "$BACKUP_TARGET_REPO" ]; then die "Target $BACKUP_TARGET_REPO does not exist" else if [ ! $PREFIX ]; then die "Could not parse a usable prefix" fi fi fi # Define a target directory in the repo BACKUP_TARGET_DIR=$BACKUP_TARGET_REPO/`hostname`/$PREFIX && debug "BACKUP_TARGET_DIR=$BACKUP_TARGET_DIR" # The timestamp where previous backup time is stored. Best leave this alone. TIMESTAMP=$BACKUP_TARGET_DIR/.timestamp && debug "TIMESTAMP=$TIMESTAMP" if [ ! -e $BACKUP_TARGET_DIR ]; then if [ -w "$BACKUP_TARGET_REPO" ]; then mkdir -p $BACKUP_TARGET_DIR || die "Cannot create target directory $BACKUP_TARGET_DIR" else debug "`stat $BACKUP_TARGET_REPO`" die "Repo $BACKUP_TARGET_REPO is not writable" fi else if [ ! -w "$BACKUP_TARGET_DIR" ]; then debug "`stat $BACKUP_TARGET_DIR`" die "Target Directory $BACKUP_TARGET_DIR is not writable" fi fi #### BACKUP #### info "Backing up $BACKUP_SOURCE_NODE to $BACKUP_TARGET_DIR." info "" # check to see if anything has changed since we last ran # This is a bit of a kludge, which basically looks for any # files that are newer than the .timestamp created by the last # full backup. It heads the output to one line for evaluation, so # as long as one file is returned it evals to true, and a backup is needed. # It replaces my old method that only checked for newer files in the # source directory, but did not report newer files in subdirs. # Each is prefixed with zero to ensure no nulls are evaluated. if [ -f $TIMESTAMP ]; then STAMPTIME=`cat $TIMESTAMP` && debug "STAMPTIME=$STAMPTIME" BACKUP_SOURCE_MODTIME=`find $BACKUP_SOURCE_NODE -newer $TIMESTAMP -print | ( while read file ; do get_mtime $file ; done ) | sort -r | head -1` debug "find found BACKUP_SOURCE_MODTIME=$BACKUP_SOURCE_MODTIME" if [ "0$BACKUP_SOURCE_MODTIME" -le "0$STAMPTIME" ]; then info No changes since $STAMPTIME, backup not necessary. if [ "$FORCE" ] && [ "$FORCE" = "1" ]; then info "Forcing Backup as per arguments." BACKUP_SOURCE_MODTIME=`date +%Y%m%d%H%M` && debug "forcing BACKUP_SOURCE_MODTIME to $BACKUP_SOURCE_MODTIME" else exit 0 fi else info Last change\: $BACKUP_SOURCE_MODTIME info Last backup\: $STAMPTIME info Using prefix: $PREFIX fi else # we have nothing to compare, so set modtime to now for backup BACKUP_SOURCE_MODTIME=`date +%Y%m%d%H%M` && debug "no timestamp so setting BACKUP_SOURCE_MODTIME to $BACKUP_SOURCE_MODTIME" fi # Execute the pre-process command and check it's status, exit if failed. if [ "$PREPROCESS_CMD" ]; then info "Executing pre-process command:" "$PREPROCESS_CMD" $PREPROCESS_CMD || ( RET=$? ; debug "pre-process command $PREPROCESS_CMD failed" = $RET ; exit $RET ) sleep 5 else debug "No pre-process command to run." fi # # Delete old backups if [ $TRUNCATE ] && [ "$TRUNCATE" = "ON" ]; then debug "Truncate is $TRUNCATE" pushd $BACKUP_TARGET_DIR > /dev/null 2>&1 || die "cannot pushd $BACKUP_TARGET_REPO" info "Maximum number of files to retain: $QUOTA" LOGCOUNT=`ls -1 | wc -l` info "Current number of backup files: $LOGCOUNT" while (( $LOGCOUNT >= $QUOTA )) do debug "while in loop, LOGCOUNT=$LOGCOUNT" oldfile=`ls -1t | tail -1` info "Truncating old file: $oldfile " rm -f $oldfile ((LOGCOUNT = LOGCOUNT - 1)) done popd >/dev/null fi # Compose a filename for tar to create BACKUP_TARGET_FILE="$PREFIX"_$BACKUP_SOURCE_MODTIME.tgz && debug "BACKUP_TARGET_FILE=$BACKUP_TARGET_FILE" info_n "Creating backup file: $BACKUP_TARGET_FILE " # determine if the source is a directory or a file and perform backup accordingly # Detect if GZIP is installed. If not just pipe to cat. if [ "`gzip -h 2>/dev/null | head -1`" ]; then debug "gzip was found" & GZIP="gzip -c" else debug "gzip not found" & GZIP=cat fi if [ -d $BACKUP_SOURCE_NODE ]; then pushd $BACKUP_SOURCE_NODE >/dev/null ( tar cf - * | $GZIP > $BACKUP_TARGET_DIR/$BACKUP_TARGET_FILE ) && echo $BACKUP_SOURCE_MODTIME >$TIMESTAMP else pushd ${BACKUP_SOURCE_NODE%/*} # just the parent directory of the source file ( tar cf - $BACKUP_SOURCE_NODE | $GZIP > $BACKUP_TARGET_DIR/$BACKUP_TARGET_FILE ) && echo $BACKUP_SOURCE_MODTIME >$TIMESTAMP fi info_tag "...done." popd >/dev/null ### CHANGE PERMISSIONS ### if [ $CHMOD ] && [ "$CHMOD" = "YES" ]; then if [ $CHMOD_MODE ] && [ "$CHMOD_MODE" -lt "778" ]; then chmod -R $CHMOD_MODE $BACKUP_TARGET_DIR || debug "`stat $BACKUP_TARGET_DIR`" else warn "CHMOD is ON but the mode $CHMOD_MODE is invalid." fi fi ### Run Post-process command if [ "$POSTPROCESS_CMD" ]; then info "Executing post-process command:" "$POSTPROCESS_CMD" $POSTPROCESS_CMD || ( RET=$? ; debug "post-process command $POSTPROCESS_CMD failed" \= $RET; exit $RET ) else debug "No post-process command to run." fi #### END BACKUP #### info "Backup complete." && echo exit 0