#!/usr/pkg/bin/bash

########################################################################
#   SaraB - Schedule And Rotate Automatic Backups
#   Copyright (C) 2008  Tristan R. Rhodes, Hiran Chaudhuri
#
#   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 2 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, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   The author, Tristan Rhodes, can be contacted by email at
#      tristanbob@hotmail.com
########################################################################

sarab_version="1.0.0"

########################################################################
########################################################################
#
#   Define functions that will be used later
#
########################################################################
########################################################################

## Rotates the rotation.schedule file by one line.  Top line moves to the bottom line.
function rotate 
{
    lines=$(cat "$SARAB_ETC/$ROTATION_SCHEDULE" | wc -l)
    firstline=$(head -n 1 "$SARAB_ETC/$ROTATION_SCHEDULE")

    # Copy all but the first line back into rotation schedule
    tail -n $(expr $lines - 1) "$SARAB_ETC/$ROTATION_SCHEDULE" > "$SARAB_ETC/rotation.schedule.temp"

    # Append the first line to the end of rotation.schedule
    echo $firstline >> "$SARAB_ETC/rotation.schedule.temp"
    mv "$SARAB_ETC/rotation.schedule.temp" "$SARAB_ETC/$ROTATION_SCHEDULE"
}

## Finds the next virtual-tape information, rotating as needed.  Disregards all comments.
function get_nextarchive
{
    # Find the total lines in the file, so we can prevent infinite loops
    TOTAL_LINES=$(cat "$SARAB_ETC/$ROTATION_SCHEDULE" | wc -l)

    # Record the current first line in the file
    THIS_LINE=$(head -n 1 "$SARAB_ETC/$ROTATION_SCHEDULE")

    # Get value of the first line, eliminating any comments
    DATA="$(echo $THIS_LINE | cut -f 1 -d'#')"
    COUNT=1
    
    # Loop until DATA is found or all lines have been processed  
    until [ $COUNT -gt $TOTAL_LINES ] || [ "$DATA" != "" ]; do  
          
	  # Rotate rotation.schedule by one line
          rotate
          if [ $? != 0 ]; then
             echo "ERROR: Error when rotating the rotation.schedule file $SARAB_ETC/$ROTATION_SCHEDULE"
             exit 1
          fi
  
          # Record the current first line in the file
          THIS_LINE=$(head -n 1 "$SARAB_ETC/$ROTATION_SCHEDULE")

          # Look for data that is before any comments
          DATA="$(echo $THIS_LINE | cut -f 1 -d'#')"

          # Increment line count 
          let COUNT+=1
	  
    done

    # Return the data. If none was found, this will return blank.
    echo $DATA
}

# Verbose output function
function verbose
{
    if [ "$SARAB_VERBOSE" = "yes" ]; then
       echo $*
    fi
}

function main
{

    ## Record start time
    START_TIME=$(date +'%s')

    # no warning so far
    WARNING=0

    # Verify input data
    if [ "$DESTINATION" = "" ] || [ "$WORK_DIR" = "" ]; then
        echo "ERROR: You must specify the DESTINATION and WORK_DIR variables in sarab.conf"
        exit 1
    fi

    echo "****************************************************************************"
    echo "Backup started at $(date)"
    verbose "Reading settings from: $SARAB_CONF"
    verbose "Using Dar configuration file named: $SARAB_ETC/$SARAB_DCF"
    verbose "Using rotation.schedule named: $SARAB_ETC/$ROTATION_SCHEDULE"
    verbose "Virtual-tapes are located under: $DESTINATION"
    verbose "Log file located at: $LOG_FILE"
    verbose

    # Create new configuration file for security vulnerable settings
    echo -n "" >"$SECURITY_CONFIG" || exit 1
    chmod 600 "$SECURITY_CONFIG" || exit 1

    ## If destination directory does not exist, create the directory
    if [ ! -d "$DESTINATION/" ]; then
        verbose "Destination directory does not exist, creating directory... mkdir -p $DESTINATION/"
        mkdir -p "$DESTINATION/"
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when creating the destination directory."
            exit 1
        fi
    fi

    ## If working directory does not exist, create an empty directory to save the archive in
    if [ ! -d "$DESTINATION/$WORK_DIR" ]; then
        verbose "Working directory does not exist, creating directory..."
        verbose "mkdir -p $DESTINATION/$WORK_DIR"
        mkdir -p "$DESTINATION/$WORK_DIR"
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when creating the working directory."
            exit 1
        fi
    else
        ## Remove files from working directory
        verbose "Removing temporary files..."
        verbose "rm -f $DESTINATION/$WORK_DIR/*"
        rm -f "$DESTINATION/$WORK_DIR/*"
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when removing working directory files."
            exit 1
        fi
    fi

    ## Record the current line from rotation.schedule
    echo
    echo "Reading the top line of the rotation.schedule file..."
    CURRENT_LINE=$(get_nextarchive)
    if [ "$?" != "0" ]; then
        echo "ERROR: Error when processing the rotation.schedule file."
        exit 1
    fi

    ## Test if the rotation schedule has any uncommented data in it
    if [ "$CURRENT_LINE" = "" ]; then
        echo "ERROR: The rotation.schedule file does not appear to have any data."
        exit 1
    fi

    verbose "Top line of rotation.schedule: $CURRENT_LINE"

    ## Record the number of fields in the current line, to see if it is a differential/incremental backup
    NUM_FIELDS=$(echo $CURRENT_LINE | wc -w)
    verbose "Number of fields: $NUM_FIELDS"

    ## If more than two fields, the data in rotation.schedule is not in the right format
    if [ "$NUM_FIELDS" -gt "2" ]; then
        echo "ERROR: Each line in rotation.schedule can only have one or two fields."
        exit 1
    fi

    ## Two fields means this is a diffential/incremental backup, one field means this is a full backup   
    if [ "$NUM_FIELDS" -eq "2" ]; then
        # First field is the current archive
        CURRENT_ARCHIVE=$(echo $CURRENT_LINE | cut -f 1 -d" ")
        # Second field is the reference archive
        REFERENCE_ARCHIVE=$(echo $CURRENT_LINE | cut -f 2 -d" ")
        # Test to see if the reference archive actually exists
        if [ -d "$DESTINATION/$REFERENCE_ARCHIVE/" ]; then     # The reference archive exists  
            REFERENCE_BASENAME="--ref $(/bin/ls $DESTINATION/$REFERENCE_ARCHIVE/*.dar | head -n 1 | sed -e 's/\.[0-9]*\.dar$//')"
            # Record information about the reference archive to include in the current archive
            echo "The reference archive for this backup was:" > $DESTINATION/$WORK_DIR/reference_archive.txt
            echo "$(ls -ltr $DESTINATION/$REFERENCE_ARCHIVE/*.dar)" >> "$DESTINATION/$WORK_DIR/reference_archive.txt"
        else                              # The reference archive does not exist
            REFERENCE_BASENAME=""          # Do not use a reference archive, do a full-backup
        fi
    else                                 # Only one field, do a full-backup
        CURRENT_ARCHIVE=$CURRENT_LINE
        REFERENCE_ARCHIVE=""
        REFERENCE_BASENAME=""
    fi

    # Check if archive shall be encrypted
    if [ "$SARAB_KEY" != "" ]
    then
        DAR_ENCRYPTION="--key $SARAB_KEY"
        if [ "$REFERENCE_ARCHIVE" != "" ]
        then
            # we have a reference archive that has probably been encrypted using $SARAB_KEY
            DAR_ENCRYPTION="$DAR_ENCRYPTION --key-ref $SARAB_KEY"
        fi

        # write arguments to secure configuration file
        echo $DAR_ENCRYPTION >>"$SECURITY_CONFIG"
    fi

    # Output to the user what type of backup is being made, and to which virtual-tapes
    if [ "$REFERENCE_ARCHIVE" = "" ]; then   # The user wants a full-backup
        echo "Creating a full-backup on the virtual-tape named: $CURRENT_ARCHIVE"
    elif [ "$REFERENCE_BASENAME" != "" ]; then  # The user wants an incremental/referential backup
        # Find out if this is an incremental or diffential backup by the presence of the reference_archive.txt file
        if [ -f "$DESTINATION/$REFERENCE_ARCHIVE/reference_archive.txt" ]
        then
            echo "Creating an incremental backup on the virtual-tape named: $CURRENT_ARCHIVE"  
        else
            echo "Creating a differential backup on the virtual-tape named: $CURRENT_ARCHIVE"
        fi
        echo "The reference virtual-tape is named: $REFERENCE_ARCHIVE"
        verbose "The basename of the reference virtual-tape is: $REFERENCE_BASENAME"
    else    # The user wants an incremental/differential backup, but the reference virtual-tape does not exist
        echo "Warning: You specified an incremental/differential backup, but the reference virtual-tape named $REFERENCE_ARCHIVE does not exist."
        echo "Creating a full-backup on the virtual-tape named: $CURRENT_ARCHIVE"
    fi
    if [ "$DAR_ENCRYPTION" != "" ]
    then
        echo "The archive will be encrypted (cipher/pass phrase not logged)."
    fi

    ## Create the backup in the working directory
    echo
    echo -n "Creating backup with DAR..."

    verbose
    verbose $DAR_BINARY --batch "$SECURITY_CONFIG" --batch "$SARAB_ETC/$SARAB_DCF" -c "$DAR_CREATE" --noconf $REFERENCE_BASENAME
	$DAR_BINARY --batch "$SECURITY_CONFIG" --batch "$SARAB_ETC/$SARAB_DCF" -c "$DAR_CREATE" --noconf $REFERENCE_BASENAME
    DAR_EXIT_CODE=$?

    if [ "$DAR_EXIT_CODE" != "0" ] && [ "$DAR_EXIT_CODE" != "5" ] && [ "$DAR_EXIT_CODE" != "11" ]; then
        # If DAR exits with a non zero, but not 5 or 11 exit code then a serious 
        # error occured.  Record the error and exit.
        echo "ERROR: Error when executing the backup with DAR. The attempted command was... "
        echo $DAR_BINARY --batch "$SECURITY_CONFIG" --batch "$SARAB_ETC/$SARAB_DCF" -c "$DAR_CREATE" --noconf $REFERENCE_BASENAME
        echo "DAR exited with $DAR_EXIT_CODE."
		echo "Find an explanation at http://dar.linux.free.fr/doc/man/dar.html#EXIT%20CODES"
        exit 1
    fi

    # If DAR exits with error code 5, a file could not be opened or read during the backup
    # process, which is more of a warning than a serious error, although it
    # sort of depends on what you are backing up.  So log it but don't exit,
    # which allows SarahB to continue, because exiting here causes the whole
    # backup to be lost which is worse than one bad file.
    if [ "$DAR_EXIT_CODE" == "5" ]; then
        echo "WARNING: A file could not be opened or read. DAR exited with 5. The attempted command was..."
        echo $DAR_BINARY --batch "$SECURITY_CONFIG" --batch "$SARAB_ETC/$SARAB_DCF" -c "$DAR_CREATE" --noconf $REFERENCE_BASENAME
		echo "Find an explanation at http://dar.linux.free.fr/doc/man/dar.html#EXIT%20CODES"
        RESULT=2
    fi

    # If DAR exits with error code 11, a file was modified during the backup
    # process, which is more of a warning than a serious error, although it
    # sort of depends on what you are backing up.  So log it but don't exit,
    # which allows SarahB to continue, because exiting here causes the whole
    # backup to be lost which is worse than one bad file.
    if [ "$DAR_EXIT_CODE" == "11" ]; then
        echo "WARNING: A file was modified during the backup process. DAR exited with 11. The attempted command was..."
        echo $DAR_BINARY --batch "$SECURITY_CONFIG" --batch "$SARAB_ETC/$SARAB_DCF" -c "$DAR_CREATE" --noconf $REFERENCE_BASENAME
		echo "Find an explanation at http://dar.linux.free.fr/doc/man/dar.html#EXIT%20CODES"
        RESULT=2
    fi


    # If configured, test the new archive for errors before removing old archive.
    if [ $TEST_ARCHIVE = "yes" ]; then
        echo
        echo -n "Testing the archive for errors..."
        verbose
        verbose $DAR_BINARY -t $DESTINATION/$WORK_DIR/$BASENAME --noconf -Q
        $DAR_BINARY --batch "$SECURITY_CONFIG" -t "$DESTINATION/$WORK_DIR/$BASENAME" --noconf -Q
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when testing the archive. The attempted command was... "
            echo "$DAR_BINARY -t \"$DESTINATION/$WORK_DIR/$BASENAME\" --noconf -Q"
            echo "DAR exited with $DAR_EXIT_CODE."
            echo "Find an explanation at http://dar.linux.free.fr/doc/man/dar.html#EXIT%20CODES"
            exit 1
        fi
    fi

    # we no longer need the secure configuration
    rm -f $SECURITY_CONFIG

    verbose
    # Remove the old backup with the same name, if it exists
    if [ -d "$DESTINATION/$CURRENT_ARCHIVE/" ]; then
        verbose "Removing old backup files..."
        verbose "rm -f $DESTINATION/$CURRENT_ARCHIVE/*"
        rm -f $DESTINATION/$CURRENT_ARCHIVE/*
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when removing the old archive files. The attempted command was... "
            echo "rm -f $DESTINATION/$CURRENT_ARCHIVE/* "
            exit 1
        fi
        verbose "rmdir $DESTINATION/$CURRENT_ARCHIVE/"
        rmdir $DESTINATION/$CURRENT_ARCHIVE/
        if [ "$?" != "0" ]; then
            echo "ERROR: Error when removing the old archive directory. The attempted command was... "
            echo "rmdir -f $DESTINATION/$CURRENT_ARCHIVE/ "
            exit 1
        fi
    fi   

    # Create an empty directory to move the archive into
    verbose "Creating virtual-tape directory... mkdir $DESTINATION/$CURRENT_ARCHIVE/"
    mkdir "$DESTINATION/$CURRENT_ARCHIVE/"

    # Move the new backup files to the correct location
    verbose "Moving files to the correct virtual-tape..."
    verbose "mv $DESTINATION/$WORK_DIR/* $DESTINATION/$CURRENT_ARCHIVE/"
    mv "$DESTINATION/$WORK_DIR"/* "$DESTINATION/$CURRENT_ARCHIVE/"
    MV_EXIT_CODE=$?
    if [ "$MV_EXIT_CODE" != "0" ]; then
        echo "ERROR: Error when moving the new archive. The attempted command was... "
        echo "mv $DESTINATION/$WORK_DIR/* $DESTINATION/$CURRENT_ARCHIVE/"
	echo "mv exited with exit code $MV_EXIT_CODE"
        exit 1
    fi

    # Copy the statically-compiled DAR executable with the arhive
    if [ "$COPY_DAR" = "yes" ]; then
        if [ -f "$DAR_STATIC" ]; then
            verbose "Copying the static Dar executable..."
            verbose "cp $DAR_STATIC $DESTINATION/$CURRENT_ARCHIVE/"
            cp "$DAR_STATIC" "$DESTINATION/$CURRENT_ARCHIVE/"
            if [ "$?" != "0" ]; then
                echo "ERROR: Error when copying the static Dar executable.  The attempted command was... "
                echo "cp $DAR_STATIC $DESTINATION/$CURRENT_ARCHIVE/"
                exit 1
            fi
        else
            echo
            echo "Warning: SaraB tried to copy the static Dar executable, but could not find it."
            echo "Skipping copy of static Dar executable."
	    RESULT=2
        fi
    fi

    # Rotate the rotation.schedule file by one line.
    echo
    echo "Rotating the rotation.schedule file by one line."
    rotate
    if [ "$?" != 0 ]; then
        echo "ERROR: Error when rotating the rotation.schedule file."
        exit 1
    fi

    ## Record finish time
    echo "Backup completed at $(date)"
    END_TIME=$(date +'%s')

    TOTAL_TIME=$(expr $END_TIME - $START_TIME)

    HOURS=$(expr $TOTAL_TIME / 3600)
    REMAINDER=$(expr $TOTAL_TIME % 3600)
    MINUTES=$(expr $REMAINDER / 60)
    SECONDS=$(expr $REMAINDER % 60)

    ## Execute post-backup script
    if [ -x "$SARAB_ETC/sarab.post" ]; then
        echo "Executing post-backup script... "
        "$SARAB_ETC/sarab.post" $CURRENT_ARCHIVE $DESTINATION
	POST_EXIT_CODE=$?
        if [ "$POST_EXIT_CODE" != 0 ]; then
            echo
            echo "ERROR: Post-backup script execution failed, exit code $POST_EXIT_CODE."
        fi
    else
	verbose "$SARAB_ETC/sarab.post not executable - post backup script skipped."
    fi


    echo "The backup process took $HOURS hours, $MINUTES minutes and $SECONDS seconds."
    echo "****************************************************************************"

    #now exit and report if we found any warning above
    verbose "Found exit code $RESULT"
    exit $RESULT
}

########################################################################
########################################################################
#
#   Begin processing here
#
########################################################################
########################################################################

echo "*** SaraB version $sarab_version"

# per default we write no log - this will be overloaded by sarab.conf
TEMP_LOG=/dev/null

# Make sure the user has root permissions
if [ "$EUID" != "0" ]; then
    echo "You must have root permissions to run this script. Exiting."
    echo
    exit 1
fi

# Check if sarab.conf was given as a parameter
if [ -n "$1" ]; then
    # The configuration file was specified as a parameter
    SARAB_CONF="$1"
else
    # Use the default location of sarab.conf
    SARAB_CONF="/usr/pkg/etc/sarab/sarab.conf"
fi

# Make sure the sarab.conf file exists
if [ ! -f "$SARAB_CONF" ]; then
    echo "ERROR: You must have $SARAB_CONF to run SaraB."
    echo
    exit 1
fi

# Read the configuration settings
source "$SARAB_CONF"

# If sarab.conf contains encryption information (cipher/key), make sure access permissions are set
if [ "$SARAB_KEY" != "" ]
then
    DIR=`ls -alodn $SARAB_CONF`
    OWNER=`echo $DIR | awk '{print $3}'`
    PERMS=`echo $DIR | cut -c5-10`
    if [ "$OWNER" != "0" ]
    then
        echo "$SARAB_CONF must be owned by root. Exiting."
        echo
        exit 1
    fi
    if [ "$PERMS" != "------" ]
    then
        echo "Only root must have access to $SARAB_CONF. Exiting."
        echo
        exit 1
    fi
fi

## Exit if dependent files do not exist...
for ITEM in "$DAR_BINARY" "$SARAB_ETC/$SARAB_DCF" "$SARAB_ETC/$ROTATION_SCHEDULE"
do
    if [ ! -f "$ITEM" ]; then
        echo "ERROR: You must have $ITEM to continue" | tee -a "$TEMP_LOG"
        MAIN_EXIT=1
    fi
done

# make sure the logfile exists and is writable
if [ ! -w "$LOG_FILE" ]; then
    echo "Need write access to $LOG_FILE."
    MAIN_EXIT=1
fi

## Only run if error was NOT detected above
if [ "$MAIN_EXIT" != "1" ]
then
    ## If run from the command line, always output the results to the console
    if [ -z $PS1 ]; then
        # Process the archive. Display and log all output and errors
        main | tee "$TEMP_LOG" 2>&1
        MAIN_EXIT=${PIPESTATUS[0]}
    else  
        # If run from cron, do not display any output, but still log everything
        # Process the archive and record all output and errors
        main > "$TEMP_LOG" 2>&1
        MAIN_EXIT="$?"
    fi
else
    # a configuration error was detected above
    exit 1
fi

if [ "$EMAIL_SUCCESS" = "yes" ] && [ "$MAIN_EXIT" = "0" ]; then
    ## If notification of success is desired, send notification email
    CURRENT_ARCHIVE=`tail -n 1 "$SARAB_ETC/$ROTATION_SCHEDULE"`
    cat "$TEMP_LOG" | mail -s "SaraB: Successful backup for $CURRENT_ARCHIVE/$BASENAME" "$EMAIL_ADDRESS"
elif [ "$EMAIL_SUCCESS" = "yes" ] && [ "$MAIN_EXIT" = "2" ]; then
    CURRENT_ARCHIVE=`tail -n 1 "$SARAB_ETC/$ROTATION_SCHEDULE"`
    cat "$TEMP_LOG" | mail -s "SaraB: Warning during backup for $CURRENT_ARCHIVE/$BASENAME" "$EMAIL_ADDRESS"
elif [ "$EMAIL_FAILURE" = "yes" ] && [ "$MAIN_EXIT" != "0" ]; then
    ## If notification of failure is desired, send notification email
    CURRENT_ARCHIVE=`head -1 "$SARAB_ETC/$ROTATION_SCHEDULE"`
    cat "$TEMP_LOG" | mail -s "SaraB: Backup failure for $CURRENT_ARCHIVE/$BASENAME" "$EMAIL_ADDRESS"
fi

# Append temporary log to main log file
cat "$TEMP_LOG" >> "$LOG_FILE" || exit 1

# Remove temporary log file
rm "$TEMP_LOG"

if [ "$MAIN_EXIT" == "0" ]; then
	exit 0
elif [ "$MAIN_EXIT" == "1" ]; then
	exit 1
elif [ "$MAIN_EXIT" == "2" ]; then
	exit 0
fi
