406 lines
8.7 KiB
Bash
Executable File
406 lines
8.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# ----------------------------------------------------------------------
|
|
# mikes handy rotating-filesystem-snapshot utility
|
|
# ----------------------------------------------------------------------
|
|
# this needs to be a lot more general, but the basic idea is it makes
|
|
# rotating backup-snapshots of /home whenever called
|
|
# ----------------------------------------------------------------------
|
|
|
|
#unset PATH # suggestion from H. Milz: avoid accidental use of $PATH
|
|
|
|
# ------------- system commands used by this script --------------------
|
|
|
|
###BEGIN: generic tool checker
|
|
# FIXME: To reuse this function, be sure to edit this line (space separated
|
|
# list).
|
|
needed_tools="id echo mount rm mv cp touch rsync"
|
|
#
|
|
# One shall not need edit anything below this line
|
|
###
|
|
checktools() {
|
|
# This functions attempts to be generic enough to be reused in any program
|
|
# that needs to check that specific tools are present on the system
|
|
# and can be found in the PATH
|
|
var=$(echo $1 | tr '[:lower:]' '[:upper:]')
|
|
local_var="$(which $1)"
|
|
eval $var="$local_var"
|
|
if [ -z "$local_var" ]
|
|
then
|
|
echo 'NOT FOUND!'
|
|
NOTFOUND="$NOTFOUND$1 "
|
|
DIE=1
|
|
else
|
|
FOUND="$FOUND$1 "
|
|
fi
|
|
unset local_var
|
|
}
|
|
|
|
for tool in $needed_tools
|
|
do
|
|
checktools "$tool"
|
|
done
|
|
|
|
if [ -n "$DIE" ]
|
|
then
|
|
echo "Some tools were not found, please check your installation" >&2
|
|
echo "Needed tools: $needed_tools" >&2
|
|
echo -n "Not found:" >&2
|
|
for tool in $NOTFOUND
|
|
do
|
|
echo -n " $tool" >&2
|
|
done
|
|
echo '!' >&2
|
|
exit 127
|
|
fi
|
|
###END: generic tool checker
|
|
|
|
while getopts 'e:h' opt
|
|
do
|
|
case $opt in
|
|
e)
|
|
EMERG="$OPTARG"
|
|
;;
|
|
h)
|
|
help
|
|
;;
|
|
esac
|
|
done
|
|
|
|
declare -a backupIndex
|
|
declare -a backupStatus
|
|
declare -a backupStatusInfo
|
|
declare -a snmpTrapContent
|
|
RETRY=5
|
|
RETRY_DELAY=60
|
|
|
|
failRsync() {
|
|
code=$1
|
|
path="$2"
|
|
if [ -d "$2" ]
|
|
then
|
|
touch "$2/unfinished.remote-backup"
|
|
backupStatus+=( 2 )
|
|
backupStatusInfo+=( "rsync errno: $code" )
|
|
else
|
|
backupStatus+=( 1 )
|
|
backupStatusInfo+=( "rsync_errno: $code" )
|
|
fi
|
|
}
|
|
|
|
openLog() {
|
|
exec 3>&1
|
|
exec 4>&2
|
|
exec 1>> /var/log/remote-backup
|
|
exec 2>&1
|
|
}
|
|
|
|
closeLog() {
|
|
exec 1>&3
|
|
exec 2>&4
|
|
}
|
|
|
|
doBackup() {
|
|
config="$1"
|
|
if [ ! -e "$config" ]
|
|
then
|
|
echo "No backups defined, dying." >&2
|
|
echo "No backups defined, dying." >&4
|
|
exit 128
|
|
fi
|
|
source /etc/remote-backup/defaults.conf
|
|
source "$config"
|
|
|
|
backupIndex+=( "$NAME" )
|
|
echo "Debut de backup de ${REMOTE_LOCATION} vers ${SNAPSHOT_RW}/${NAME}/ a $(date +"%H:%M, le %d/%m/%Y")"
|
|
|
|
if [[ "$REMOUNT" == "true" ]]
|
|
then
|
|
# attempt to remount the RW mount point as RW; else abort
|
|
mountMsg="$( $MOUNT -o remount,rw "$MOUNT_DEVICE" "$SNAPSHOT_RW" 2>&1 )" ;
|
|
if (( $? ))
|
|
then
|
|
echo "snapshot: could not remount $SNAPSHOT_RW readwrite" >&2
|
|
backupStatus+=( 4 )
|
|
backupStatusInfo+=( "$MOUNT_DEVICE: $mountMsg" )
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [ ! -d "$SNAPSHOT_RW/$NAME" ]
|
|
then
|
|
mkdir -p "$SNAPSHOT_RW/$NAME"
|
|
fi
|
|
|
|
if [[ "$2" == emerg ]]
|
|
then
|
|
runBackup emerg.$(date +%Y%m%d%H%M)
|
|
else
|
|
if [ -f "$SNAPSHOT_RW/$NAME/.lock" ] \
|
|
&& [ -r /proc/$(<"$SNAPSHOT_RW/$NAME/.lock")/cmdline ]
|
|
then
|
|
progname=$(</proc/$(<"$SNAPSHOT_RW/$NAME/.lock")/cmdline)
|
|
progname=${progname##*/}
|
|
if [[ $progname = remote-backup ]]
|
|
then
|
|
backupStatus+=( 5 )
|
|
backupStatusInfo+=( "Process $(<"$SNAPSHOT_RW/$NAME/.lock") still running" )
|
|
else
|
|
echo $$ > "$SNAPSHOT_RW/$NAME/.lock"
|
|
rotateBackup
|
|
runBackup daily.0
|
|
rm "$SNAPSHOT_RW/$NAME/.lock"
|
|
fi
|
|
else
|
|
echo $$ > "$SNAPSHOT_RW/$NAME/.lock"
|
|
rotateBackup
|
|
runBackup daily.0
|
|
rm "$SNAPSHOT_RW/$NAME/.lock"
|
|
fi
|
|
fi
|
|
|
|
# now remount the RW snapshot mountpoint as readonly
|
|
if [[ "$REMOUNT" == "true" ]]
|
|
then
|
|
$MOUNT -o remount,ro "$MOUNT_DEVICE" "$SNAPSHOT_RW"
|
|
if (( $? ))
|
|
then
|
|
echo "snapshot: could not remount $SNAPSHOT_RW readonly" >&2
|
|
fi
|
|
fi
|
|
|
|
echo "Fin de backup de ${REMOTE_LOCATION} vers ${SNAPSHOT_RW}/${NAME}/ a $(date +"%H:%M, le %d/%m/%Y")"
|
|
echo '-----------------------------------------------------------------------'
|
|
}
|
|
|
|
rotateBackup() {
|
|
# rotating snapshots
|
|
if [ -f "$SNAPSHOT_RW/$NAME/daily.0" ]
|
|
then
|
|
echo "Previous backup failed, skipping rotation."
|
|
rm "$SNAPSHOT_RW/$NAME/daily.0"
|
|
elif [ -f "$SNAPSHOT_RW/$NAME/daily.0/unfinished.remote-backup" ]
|
|
then
|
|
echo "Previous backup interrupted, skipping rotation."
|
|
rm "$SNAPSHOT_RW/$NAME/daily.0/unfinished.remote-backup"
|
|
else
|
|
# step 1: delete the oldest snapshot, if it exists:
|
|
if [ -d "$SNAPSHOT_RW/$NAME/daily.${MAX_ROTATE}" ]
|
|
then
|
|
$RM -rf "$SNAPSHOT_RW/$NAME/daily.${MAX_ROTATE}"
|
|
fi
|
|
|
|
# step 2: shift the snapshots(s) back by one, if they exist
|
|
for (( backup_num=$MAX_ROTATE ; backup_num >= 0 ; backup_num-- ))
|
|
do
|
|
if [ -d "$SNAPSHOT_RW/$NAME/daily.${backup_num}" ]
|
|
then
|
|
$MV -f "$SNAPSHOT_RW/$NAME/daily.${backup_num}" \
|
|
"$SNAPSHOT_RW/$NAME/daily.$(( backup_num + 1 ))"
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
runBackup() {
|
|
# step 3: rsync from the system, linking to latest snapshot if files are
|
|
# identical.
|
|
RSYNC_OPTS="-a --exclude-from=$EXCLUDES --bwlimit=$BWLIMIT"
|
|
|
|
if [[ $2 == PROTOCOLMISMATCH ]]
|
|
then
|
|
# Protocol mismatch occurred, maybe caused by multiple --link-dest
|
|
# occurrences with rsync server protocol < 29
|
|
proto29=1
|
|
MAX_ROTATE=1
|
|
fi
|
|
for (( backup_num=1 ; backup_num <= MAX_ROTATE ; backup_num++ ))
|
|
do
|
|
if [ -d "$SNAPSHOT_RW/$NAME/daily.$backup_num" ]
|
|
then
|
|
RSYNC_OPTS="$RSYNC_OPTS --link-dest=$SNAPSHOT_RW/$NAME/daily.$backup_num"
|
|
fi
|
|
done
|
|
for old_backup in "$SNAPSHOT_RW/$NAME/emerg".*
|
|
do
|
|
if [ -d "$old_backup" ]
|
|
then
|
|
RSYNC_OPTS="$RSYNC_OPTS --link-dest=$old_backup"
|
|
fi
|
|
done
|
|
if (( TIMEOUT ))
|
|
then
|
|
RSYNC_OPTS="$RSYNC_OPTS --timeout=$TIMEOUT"
|
|
else
|
|
RSYNC_OPTS="$RSYNC_OPTS --timeout=1800"
|
|
fi
|
|
|
|
# Not root? use --fake-super
|
|
if (( UID > 0 ))
|
|
then
|
|
echo "(info) running as user, using --fake-super."
|
|
RSYNC_OPTS="$RSYNC_OPTS --fake-super"
|
|
fi
|
|
|
|
|
|
for (( try=1 ; try <= RETRY ; try++ ))
|
|
do
|
|
$RSYNC \
|
|
$RSYNC_OPTS \
|
|
$ADDITIONAL_RSYNC_OPTS \
|
|
"${REMOTE_LOCATION}" \
|
|
"$SNAPSHOT_RW/$NAME/$1"
|
|
|
|
returncode=$?
|
|
case $returncode in
|
|
0)
|
|
backupStatus+=( 0 )
|
|
backupStatusInfo+=( "Complete" )
|
|
break
|
|
;;
|
|
2)
|
|
break
|
|
;;
|
|
*)
|
|
if (( try == RETRY ))
|
|
then
|
|
cat <<-EOF
|
|
|
|
Trial $try / $RETRY failed, Giving up!
|
|
|
|
EOF
|
|
else
|
|
cat <<-EOF
|
|
|
|
Trial $try / $RETRY failed.
|
|
Sleeping $RETRY_DELAY...
|
|
|
|
EOF
|
|
sleep $RETRY_DELAY
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
case $returncode in
|
|
0)
|
|
:
|
|
;;
|
|
2)
|
|
if (( proto29 ))
|
|
then
|
|
failRsync PROTO_MISMATCH
|
|
else
|
|
cat <<-EOF
|
|
|
|
Rsync protocol mismatch, trying with only one --link-dest
|
|
|
|
EOF
|
|
runBackup "$1" PROTOCOLMISMATCH
|
|
fi
|
|
;;
|
|
*)
|
|
failRsync $returncode "$SNAPSHOT_RW/$NAME/$1"
|
|
;;
|
|
esac
|
|
|
|
# step 5: update the mtime of daily.0 to reflect the snapshot time
|
|
$TOUCH "$SNAPSHOT_RW/$NAME/$1"
|
|
|
|
# and thats it
|
|
unset proto29
|
|
}
|
|
|
|
mailStatus() {
|
|
backup=$1
|
|
if (( backupStatus[$backup] != 0 ))
|
|
then
|
|
echo -n "${backupIndex[$backup]} failed: "
|
|
case ${backupStatus[$backup]} in
|
|
1) #RSYNC_FAILED
|
|
echo "No files transferred - ${backupStatusInfo[$backup]}"
|
|
;;
|
|
2) #RSYNC_PARTIAL
|
|
echo "Partial transfer, will *NOT* get rotated - ${backupStatusInfo[$backup]}"
|
|
;;
|
|
3) #PROTO_MISMATCH
|
|
echo "No files transferred - Protocol mismatch - Upgrade rsync daemon"
|
|
;;
|
|
4) #REMOUNT_RW
|
|
echo "${backupStatusInfo[$backup]}"
|
|
;;
|
|
5) #LOCKED
|
|
echo "Previous backup still running."
|
|
;;
|
|
*) #OTHER
|
|
echo "$info $moreinfo"
|
|
;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
snmpStatus() {
|
|
# Id. 1.3.6.1.4.1.39402.1.1.2.1
|
|
# Name 1.3.6.1.4.1.39402.1.1.2.2
|
|
# Status 1.3.6.1.4.1.39402.1.1.2.3.1
|
|
# Stat. Inf. 1.3.6.1.4.1.39402.1.1.2.3.2
|
|
backup=$1
|
|
snmpTrapContent+=( "1.3.6.1.4.1.39402.1.1.2.1.$backup" )
|
|
snmpTrapContent+=( "i" )
|
|
snmpTrapContent+=( "$backup" )
|
|
snmpTrapContent+=( "1.3.6.1.4.1.39402.1.1.2.2.$backup" )
|
|
snmpTrapContent+=( "s" )
|
|
snmpTrapContent+=( "${backupIndex[$backup]}" )
|
|
snmpTrapContent+=( "1.3.6.1.4.1.39402.1.1.2.3.1.$backup" )
|
|
snmpTrapContent+=( "i" )
|
|
snmpTrapContent+=( "${backupStatus[$backup]}" )
|
|
snmpTrapContent+=( "1.3.6.1.4.1.39402.1.1.2.3.2.$backup" )
|
|
snmpTrapContent+=( "s" )
|
|
snmpTrapContent+=( "${backupStatusInfo[$backup]}" )
|
|
}
|
|
|
|
openLog
|
|
|
|
if [ -n "$EMERG" ]
|
|
then
|
|
doBackup /etc/remote-backup/"${EMERG//\//_}"/config emerg
|
|
else
|
|
for config in /etc/remote-backup/*/config
|
|
do
|
|
doBackup "$config"
|
|
done
|
|
fi
|
|
|
|
closeLog
|
|
|
|
if (( EMAIL ))
|
|
then
|
|
for backup in ${backupStatus[@]}
|
|
do
|
|
if (( $backup ))
|
|
then
|
|
cat <<-EOF
|
|
Some backups exited non-zero.
|
|
See the logfile for more detail.
|
|
|
|
EOF
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
for backup in ${!backupIndex[@]}
|
|
do
|
|
(( EMAIL )) && mailStatus $backup
|
|
snmpStatus $backup
|
|
done
|
|
|
|
if [ -n "$SNMPCOMM" -a -n "$SNMPDEST" ]
|
|
then
|
|
read uptime discard < /proc/uptime
|
|
uptime=$(( ${uptime//./} % (2**32)))
|
|
unset discard
|
|
for dest in ${SNMPDEST[@]}
|
|
do
|
|
snmptrap -v 2c -c $SNMPCOMM $dest $uptime 1.3.6.1.4.1.39402.1.1.1 "${snmpTrapContent[@]}"
|
|
done
|
|
fi
|