#!/bin/sh
# -----------------------------------------------------------------------
# $Id: snapshot_rotate 2.sh 2007-01-16 10:19:31Z xp $
#
# Copyright 2006 Xavier Perseguers <xavier.perseguers@skutale.ch>
#
# Xavier Perseguers handy rotating-filesystem-snapshot utility
# -----------------------------------------------------------------------
# This needs to be a lot more general, but the basic idea is, it makes
# rotating backup-snapshots of important directories
# -----------------------------------------------------------------------

unset PATH       # suggestion from H. Milz: avoid accidental use of $PATH

# -------------- user variables -----------------------------------------

# will create hourly.0, hourly.1, ... hourly.NUMBER_OF_HOURLY_BACKUPS - 1
NUMBER_OF_HOURLY_BACKUPS=2

# will create daily.0, daily.1, ... daily.6
# THIS IS NOT CONFIGURABLE

# will create weekly.0, weekly.1, ... weekly.NUMBER_OF_WEEKLY_BACKUPS - 1
NUMBER_OF_WEEKLY_BACKUPS=4

# to use FQDN instead, use `/bin/hostname -f`
MACHINE_NAME=`/bin/hostname -f`

# -------------- system commands used by this script --------------------

ID=/usr/bin/id
BASENAME=/usr/bin/basename
CAT=/bin/cat
CP=/bin/cp
CUT=/usr/bin/cut
DATE=/bin/date
ECHO=/bin/echo
EXPR=/usr/bin/expr
FIND=/usr/bin/find
GREP=/bin/grep
HEAD=/usr/bin/head
MV=/bin/mv
MKDIR=/bin/mkdir
MOUNT=/bin/mount
RM=/bin/rm
SED=/bin/sed
SENDMAIL=/usr/sbin/sendmail
SEQ=/usr/bin/seq
SORT=/usr/bin/sort
SLEEP=/bin/sleep
STAT=/usr/bin/stat
TAR=/bin/tar
TOUCH=/bin/touch
UMOUNT=/bin/umount
RSYNC=/usr/bin/rsync

# -------------- file locations -----------------------------------------

MOUNT_DEVICE=172.16.0.1:/backup
SNAPSHOT_RW=/backup
EXCLUDES=/root/scripts/backup_exclude

# -------------- backup functions ---------------------------------------

# create dvd-DATE
#
function dvd_create() {
    LAST_NUM=$($EXPR $NUMBER_OF_WEEKLY_BACKUPS - 1)
    if [ -f $DEST/weekly.$LAST_NUM.tar.gz ]; then
        DATE=$($STAT -c "%y" $DEST/weekly.$LAST_NUM.tar.gz | $SED 's/ .*//')
        $MKDIR -p $DEST/backup_to_dvd
        $MV $DEST/weekly.$LAST_NUM.tar.gz $DEST/backup_to_dvd/dvd-$DATE.tar.gz
    fi
}

# create weekly.0, weekly.1, ... weekly.NUMBER_OF_WEEKLY_BACKUPS - 1
#
function weekly_rotate() {

    # step 1: delete the oldest weekly snapshot, if it exists
    #
    LAST_NUM=$($EXPR $NUMBER_OF_WEEKLY_BACKUPS - 1)
    if [ -f $DEST/weekly.$LAST_NUM.tar.gz ]; then
        #$RM -f $DEST/weekly.$LAST_NUM.tar.gz
        dvd_create
    fi

    # step 2: shift the middle snapshot(s) back by one, if they exist
    #
    LAST_NUM=$($EXPR $NUMBER_OF_WEEKLY_BACKUPS - 2)
    for i in `$SEQ $LAST_NUM -1 0`;
    do
        j=$($EXPR $i + 1)
        if [ -f $DEST/weekly.$i.tar.gz ]; then
            $MV $DEST/weekly.$i.tar.gz $DEST/weekly.$j.tar.gz
        fi
    done

    # step 3: make a gziped tar of the latest daily snapshot
    #
    N=$($FIND $DEST/daily.* -maxdepth 0 -exec $BASENAME {} \; | $CUT -d. -f2 | $SORT -rn | $HEAD -1)
    if [ -d $DEST/daily.$N ]; then
        pushd $DEST/daily.$N >/dev/null
        $TAR czf $DEST/weekly.0.tar.gz .
	popd >/dev/null

        # remove latest daily snapshot
        $RM -rf $DEST/daily.$N
    fi
}

# create daily.0, daily.1, ... daily.6
#
function daily_rotate() {

    # step 1: test if weekly rotation should occur or else delete
    #         the oldest snapshot, if it exists.  Weekly rotation
    #         occurs on Monday (DAY_WEEK = "1").  See man date for
    #         details
    #
    DAY_WEEK=$($DATE "+%u")
    if [ $DAY_WEEK = "1" ]; then
        weekly_rotate
    else
        # delete the oldest daily snapshot, if it exists

        if [ -d $DEST/daily.6 ]; then
            $RM -rf $DEST/daily.6
        fi
    fi

    # step 2: shift the middle snapshot(s) back by one, if they exist
    #
    for i in 5 4 3 2 1 0;
    do
        j=$($EXPR $i + 1)
        if [ -d $DEST/daily.$i ]; then
            $MV $DEST/daily.$i $DEST/daily.$j
        fi
    done

    # step 3: rename last hourly.* backup as daily.0
    #
    N=$($FIND $DEST/hourly.* -maxdepth 0 -exec $BASENAME {} \; | $CUT -d. -f2 | $SORT -rn | $HEAD -1)
    if [ -d $DEST/hourly.$N ]; then
        $MV $DEST/hourly.$N $DEST/daily.0
    fi

    # note: do *not* update the mtime of daily.0; it will reflect
    #       when hourly.N was made, which should be correct
}

# -------------- the script itself --------------------------------------

# make sure we are running as root
#
if (( `$ID -u` != 0 )); then
    $ECHO "Sorry, must be root.  Exiting..."
    exit
fi

# make sure there is something to backup
#
if [[ -z "$1" || ! -r $1 ]]; then
    $ECHO "snapshot: you must specify a file containing directories"
    $ECHO "          to backup;  be sure to specify absolute paths"
    $ECHO "          without trailing slash."
    exit
fi
SRC=$1

# try to avoid "State NFS file handle" error
#
cd $SNAPSHOT_RW >/dev/null 2>&1
if (( $? != 0 )); then
    $ECHO "fixing 'State NFS file handle'..."
    $UMOUNT $SNAPSHOT_RW >/dev/null 2>&1
    $SLEEP 10
    $MOUNT $MOUNT_DEVICE $SNAPSHOT_RW >/dev/null 2>&1
fi

# attempt to remount the RO mount point as RW; else abort
#
$MOUNT -o remount,rw $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
    $ECHO "snapshot: could not remount $SNAPSHOT_RW readwrite"
    $ECHO "snapshot: could not remount $SNAPSHOT_RW readwrite" | $SENDMAIL xavier@perseguers.ch
    exit
fi

# rotating snapshots of the directory to backup
#
DEST=$SNAPSHOT_RW/$MACHINE_NAME

# step 1: test if daily rotation should occur or else delete
#         the oldest snapshot, if it exists
#
HOUR=$($DATE "+%H")
if [ $HOUR = "00" ]; then
    daily_rotate
else
    # delete the oldest hourly snapshot, if it exists

    LAST_NUM=$($EXPR $NUMBER_OF_HOURLY_BACKUPS - 1)
    if [ -d $DEST/hourly.$LAST_NUM ]; then
        $RM -rf $DEST/hourly.$LAST_NUM
    fi
fi

# step 2: shift the middle snapshot(s) back by one, if they exist
#
LAST_NUM=$($EXPR $NUMBER_OF_HOURLY_BACKUPS - 2)
for i in `$SEQ $LAST_NUM -1 1`;
do
    j=$($EXPR $i + 1)
    if [ -d $DEST/hourly.$i ]; then
        $MV $DEST/hourly.$i $DEST/hourly.$j
    fi
done

# step 3: make a hard-link-only (except for dirs) copy of the latest
#         snapshot, if that exists
#
if [ -d $DEST/hourly.0 ]; then
    $CP -al $DEST/hourly.0 $DEST/hourly.1
fi

# step 4: rsync from the system into the latest snapshot (notice that
#         rsync behaves like cp --remove-destination by default, so the
#         destination is unlinked first.  If it were not so, this would
#         copy over the other snapshot(s) too!
#
for d in $($CAT $SRC | $GREP -v "^#" | $GREP -v "^$");
do
    $MKDIR -p $DEST/hourly.0$d
    $RSYNC                                 \
        -va --delete --delete-excluded     \
        --exclude-from="$EXCLUDES"         \
        $d/ $DEST/hourly.0$d

done

# step 5: update the mtime of hourly.0 to reflect the snapshot time
#
$TOUCH $DEST/hourly.0

# now remount the RW snapshot mountpoint as readonly
#
$MOUNT -o remount,ro $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
    $ECHO "snapshot: could not remount $SNAPSHOT_RW readonly"
    exit
fi

