A cheap snapshot system (Part 3)

People delete files by accident and the work is lost if you do not have a system that takes periodic snapshots of the system to be able to recover files, however a lot of “snapshot” systems cost a lot of money, something a SME never has enough of, so I wrote my own little utility with the help of some “rotate” utility I found on the Internet using one of the best features Linux has called “hard-links”.

Please checkout the previous blog entries for this:

  1. http://www.systemadministratorblog.com/2014/07/a-cheap-snapshot-system-part-1/
  2. http://www.systemadministratorblog.com/2014/07/a-cheap-snapshot-system-part-2/

Now for the Utility:

#!/bin/bash
# ———————————————————————-
# mikes handy rotating-filesystem-snapshot utility
# heavily changed by Jobst Schmalenbach
# ———————————————————————-# make a local path
export PATH=/usr/local/bin:/usr/bin:/bin# values
BACKUP_POINT=”/snapshot”
BACKUP_TYPE=””
FILE_SET=””# this directory
UTIL_DIR=”/usr/local/backup”# binaries
MOUNT=”/bin/mount”
UMOUNT=”/bin/umount”
RM=”/bin/rm”
MV=”/bin/mv”
CP=”/bin/cp”
TOUCH=”/bin/touch”
RSYNC=”/usr/bin/rsync”
MKDIR=”/bin/mkdir”
SSH=”/usr/bin/ssh”
CAT=”/bin/cat”
DF=”/bin/df”
DATE=”/bin/date”
GREP=”/bin/grep”
LS=”/bin/ls”
ID=”/usr/bin/id”
ECHO=”/bin/echo”
GETOPT=”/usr/bin/getopt”
SEQ=”/usr/bin/seq”# debug option.
DEBUG=1

############################################################################### functions
rotate_snapshots()
{
# parms:
# 1: destination dir
# 2: backup SOURCE name
# 3: backup TARGET name
# 4: highest backup number
# 5: highest backup SOURCE number

# delete the oldest snapshot in background, if it exists:
$ECHO -n “Rotating snapshots for $1 ($2-$3) .. ”
if [ -d $BACKUP_POINT/$1/$3.$4 ] ; then
COMMAND=”$MV $BACKUP_POINT/$1/$3.$4 $BACKUP_POINT/$1/$3.$4.delete”
if [ $DEBUG == 1 ]; then $ECHO $COMMAND; fi
$COMMAND
RETVAL=$?
if [ $RETVAL -ne 0 ] ; then
$ECHO “ERROR: $MV $BACKUP_POINT/$1/$3.$4 $BACKUP_POINT/$1/$3.$4.delete”
exit 1
fi
COMMAND=”$RM -rf $BACKUP_POINT/$1/$3.$4.delete &”
if [ $DEBUG == 1 ]; then $ECHO $COMMAND; fi
$COMMAND
RETVAL=$?
if [ $RETVAL -ne 0 ] ; then
$ECHO “ERROR: $RM -rf $BACKUP_POINT/$1/$3.$4.delete &”
exit 1
fi
fi
# shift the middle snapshots(s) back by one, if they exist
for i in `$SEQ $4 -1 2` ; do
iminus=$(($i-1)) ;
if [ -d $BACKUP_POINT/$1/$3.$iminus ] ; then
# trying to preserve the MTIME of the source.
export timestamp=`stat -c %y $BACKUP_POINT/$1/$3.$iminus`
if [ $DEBUG == 1 ]; then $ECHO “Current timestamp of $BACKUP_POINT/$1/$3.$iminus is ${timestamp}”; fi
COMMAND=”$MV $BACKUP_POINT/$1/$3.$iminus $BACKUP_POINT/$1/$3.$i”
if [ $DEBUG == 1 ]; then $ECHO $COMMAND; fi
$COMMAND
RETVAL=$?
if [ $RETVAL -ne 0 ] ; then
$ECHO “ERROR: $MV $BACKUP_POINT/$1/$3.$iminus $BACKUP_POINT/$1/$3.$i”
exit 1
fi
touch –date=”${timestamp}” $BACKUP_POINT/$1/$3.$i
if [ $DEBUG == 1 ]; then $ECHO “Set timestamp of $BACKUP_POINT/$1/$3.$i to ${timestamp}”; fi
fi
done
# make a hard-link-only (except for dirs) copy of the latest snapshot,
if [ -d $BACKUP_POINT/$1/$2.$5 ] ; then
COMMAND=”$CP -al $BACKUP_POINT/$1/$2.$5 $BACKUP_POINT/$1/$3.1″
if [ $DEBUG == 1 ]; then $ECHO $COMMAND; fi
$COMMAND
RETVAL=$?
if [ $RETVAL -ne 0 ]; then
$ECHO “ERROR: $CP -al $BACKUP_POINT/$1/$2.$5 $BACKUP_POINT/$1/$3.1”
exit 1
fi
fi
$ECHO “Done”
}

usage()
{
$ECHO $1
$ECHO
$ECHO “Usage”
$ECHO “$0 -bt [hourly|daily|weekly|monthly] -fs [ static | dynamic ]”
$ECHO
exit
}

############################################################################### main

# check that arguments are given
if [ $# = 0 ] ; then
usage “Error: no arguments!”
exit;
fi

while test -n “$1”; do
case “$1” in
-bp)
BACKUP_POINT=$2
shift
;;
–backup_point)
BACKUP_POINT=$2
shift
;;
-bt)
BACKUP_TYPE=$2
shift
;;
–backup_type)
BACKUP_TYPE=$2
shift
;;
-fs)
FILE_SET=$2
shift
;;
-fileset)
FILE_SET=$2
shift
;;
-d)
DEBUG=1
;;
*)
usage “I do not know what you mean!”
;;
esac
shift
done

# check vars.
if [ “$BACKUP_TYPE” == “” ]; then
usage “BACKUP_TYPE not specified, can’t do!”
fi
if [ “$FILE_SET” == “” ]; then
usage “FILE_SET not specified, can’t do!”
fi

if [ “$FILE_SET” != “dynamic” ] ; then
if [ “$FILE_SET” != “static” ] ; then
usage “Fileset must either be ‘dynamic’ or ‘static’ …”
fi
fi

# check the mount point
if [ ! -d $BACKUP_POINT ] ; then
if [ -e $BACKUP_POINT ] ; then
$ECHO “Error: $BACKUP_POINT is not a directory. Exiting!”
exit
fi
fi
if [ ! -e $BACKUP_POINT/SnapShotDrive ] ; then
$ECHO “Error: Snapshot drive is not mounted. Exiting!”
exit
fi

# make sure we are running as root
if (( `$ID -u` != 0 )); then
usage “Error: You must be root. Exiting…”
exit
fi

# check what needs to be done
case $BACKUP_TYPE in
hourly)
BCKUP_SOURCE=”hourly”
BCKUP_TARGET=”hourly”
BCKUP_NUM=”6″
BCKUP_SOURCE_NUM=”0″
;;
daily)
BCKUP_SOURCE=”hourly”
BCKUP_TARGET=”daily”
BCKUP_NUM=”5″
BCKUP_SOURCE_NUM=”6″
;;
weekly)
BCKUP_SOURCE=”daily”
BCKUP_TARGET=”weekly”
BCKUP_NUM=”4″
BCKUP_SOURCE_NUM=”5″
;;
monthly)
BCKUP_SOURCE=”weekly”
BCKUP_TARGET=”monthly”
BCKUP_NUM=”4″
BCKUP_SOURCE_NUM=”4″
;;
*)
usage “Incorrect Parameters for BACKUP_TYPE $BACKUP_TYPE specified!”
exit 1;
esac

# debug
if [ $DEBUG == 1 ]; then
$ECHO “Current Parms: ”
$ECHO “BCKUP_SOURCE “$BCKUP_SOURCE
$ECHO “BCKUP_TARGET “$BCKUP_TARGET
$ECHO “BCKUP_NUM “$BCKUP_NUM
fi

DIR_LIST=””
if [ “$FILE_SET” == “dynamic” ] ; then
# dynamic stuff ONLY
# /var
# /home/httpd
# /samba/*
#
DIR_LIST=$DIR_LIST” home/httpd”
DIR_LIST=$DIR_LIST” home/newsletters”
DIR_LIST=$DIR_LIST” home/support”
DIR_LIST=$DIR_LIST” etc”
DIR_LIST=$DIR_LIST” var”
else
# static stuff PLUS dynamic stuff
# basically THE LOT, except things we do not need
# make up the directories we want to backup for this fileset
for d in `ls -1 /`; do
# need to EXCLUDE the ones we include BELOW in the other sections, etc samba
# also the ones never needed that are made during boot up, e.g. proc sys selinux tmp etc.
if [ “$d” == “lost+found” ] ; then
continue
elif [ “$d” == “home” ] ; then
continue
elif [ “$d” == “media” ] ; then
continue
elif [ “$d” == “mnt” ] ; then
continue
elif [ “$d” == “opt” ] ; then
continue
elif [ “$d” == “proc” ] ; then
continue
elif [ “$d” == “selinux” ] ; then
continue
elif [ “$d” == “snapshot” ] ; then
continue
elif [ “$d” == “squidcache” ] ; then
continue
elif [ “$d” == “srv” ] ; then
continue
elif [ “$d” == “sys” ] ; then
continue
elif [ “$d” == “tmp” ] ; then
continue
else
DIR_LIST=$DIR_LIST” “$d
fi
done
for d in `ls -1 /home`; do
if [ “$d” == “lost+found” ] ; then
continue
elif [ “$d” == “backup” ] ; then
continue
elif [ “$d” == “milter” ] ; then
continue
elif [ “$d” == “sueb” ] ; then
continue
elif [[ “$d” =~ “xperia” ]]; then
continue
elif [ -d “/home/”$d ] ; then
DIR_LIST=$DIR_LIST” home/”$d
fi
done
fi

# show
if [ $DEBUG == 1 ]; then
$ECHO “DIR_LIST contains: “$DIR_LIST
fi

# loop the directory list
for d in $DIR_LIST; do
$ECHO “Working with $d”

# delete the last one, rotate all but one and HARDLINK the hourly.0 to hourly.1
rotate_snapshots $d $BCKUP_SOURCE $BCKUP_TARGET $BCKUP_NUM $BCKUP_SOURCE_NUM

# only when hourly we do a sync
if [ “$BACKUP_TYPE” == “hourly” ] ; then

# the target in the snapshot
TARGET=”$BACKUP_POINT/$d/$BCKUP_SOURCE.0″

# make the dir
$ECHO -n “MKDIR $TARGET … ”
$MKDIR -p $TARGET
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
$ECHO “successfull made $TARGET”
else
$ECHO “error making $TARGET”
exit 1
fi

# display
$ECHO -n “Rsyncing /$d/ $TARGET … ”

# this is a file containing file we do not want ONLY if its there.
EXCLUDES=”$UTIL_DIR/snapshot_backup_exclude_$d”
if [ -e $EXCLUDES ] ; then
EXCLUDES=”–exclude-from=$EXCLUDES”
else
EXCLUDES=””
fi

# standard exludes for when /home is encountered
# this is a regexp that yields an empty string if HOME is encountered
t=${d##*home*}
if [ “$t” == “” ] ; then
HOME_EXCLUDE=” –exclude .snapshot* –exclude Cache/ –exclude Cache.Trash/ –exclude \”Temporary Internet Files\” ”
else
HOME_EXCLUDE=””
fi

# there’s a bug in rsync re -a option, so leave below.
COMMAND=”$RSYNC -aHAX –numeric-ids –delete –delete-excluded –exclude .snapshot –exclude \”*~\” $EXCLUDES $HOME_EXCLUDE /$d/ $TARGET”
if [ $DEBUG == 1 ]; then
$ECHO ” ”
$ECHO $COMMAND
fi
# I had a problem with the space in TempInternet, so I asked the question on SLUG, I got a reply:
# “You need bash to re-evaluate those quotes so it understands it should
# be one argument.”
eval $COMMAND
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
$ECHO “successfull rsynced $d!”
else
$ECHO “error rsyncing $d!”
fi

# update the mtime of hourly.0 to reflect the snapshot time
$ECHO -n “Updating MTIME of $TARGET .. ”
$TOUCH $TARGET
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
$ECHO “successfull touched $TARGET”
else
$ECHO “error touching $TARGET”
fi
fi

# space
$ECHO ” ”
done

# wait for rm in background finish
wait

# all done
echo
echo “All Done.”
exit

###################################################################################### end of file

1 thought on “A cheap snapshot system (Part 3)”

  1. Pingback: A cheap snapshot system (Part 2) | System Administrator Blog

Leave a Comment

Your email address will not be published. Required fields are marked *

You must tick the checkbox for 'I am not a robot' before you can submit your comment!