sync.sh: different exit status, better locking, etc...

* -l option to keep log file
* lock files are now in /tmp
* no sendmail will give better information about issue
* checks for SRC and DST dirs before backup
This commit is contained in:
2022-05-13 20:09:52 +02:00
parent 6df830554d
commit 8dad65dd90

View File

@@ -51,6 +51,8 @@
# bash's -x option) when some errors are difficult to track. # bash's -x option) when some errors are difficult to track.
# -f # -f
# Filter some rsync output, such as hard and soft links, dirs, etc. # Filter some rsync output, such as hard and soft links, dirs, etc.
# -l
# Keep log file (usually /tmp/sync-log-PID).
# -m # -m
# Display a "man-like" description and exit. # Display a "man-like" description and exit.
# -n # -n
@@ -70,8 +72,8 @@
# Enable rsync compression. Should be used when the transport is more # Enable rsync compression. Should be used when the transport is more
# expensive than CPU (typically slow connections). # expensive than CPU (typically slow connections).
# -Z # -Z
# By default, if gzip utility is available, the log attachment is # By default, if gzip utility is available, the email log attachment
# compressed. This option will prevent any compression. # is compressed. This option will prevent any compression.
# #
# GENERAL # GENERAL
# You should avoid modifying variables in this script. # You should avoid modifying variables in this script.
@@ -107,6 +109,7 @@
# Bruno Raoult. # Bruno Raoult.
# #
#%MAN_END% #%MAN_END%
#
# BUGS # BUGS
# Many. # Many.
# This was written for a "terastation" NAS server, which is a kind of # This was written for a "terastation" NAS server, which is a kind of
@@ -159,6 +162,7 @@ NUMID="" # (-u) use numeric IDs
DEBUG=n # (-D) debug: no I/O redirect (y/n) DEBUG=n # (-D) debug: no I/O redirect (y/n)
MAILTO=${MAILTO:-""} # (-n) mail recipient. -n sets it to "" MAILTO=${MAILTO:-""} # (-n) mail recipient. -n sets it to ""
ZIPMAIL="gzip" # (-Z) zip mail attachment ZIPMAIL="gzip" # (-Z) zip mail attachment
KEEPLOGFILE=n # (-l) keep log file
# options only settable in config file. # options only settable in config file.
NYEARS=3 # keep # years (int) NYEARS=3 # keep # years (int)
@@ -187,9 +191,23 @@ SCRIPT="$0" # full path to script
CMDNAME=${0##*/} # script name CMDNAME=${0##*/} # script name
PID=$$ # current pricess PID PID=$$ # current pricess PID
LOCKED=n # indicates if we created lock file. LOCKED=n # indicates if we created lock file.
SUBJECT="$CMDNAME ${*##*/}" # mail subject
ERROR=0 # set by error_handler when called
STARTTIME=$(date +%s) # time since epoch in seconds STARTTIME=$(date +%s) # time since epoch in seconds
HOSTNAME="$(hostname)"
declare -A ERROR_STR=( # error strings
[0]="ok"
[1]="error"
[2]="missing command"
[3]="source directory error"
[4]="could not create lock file"
[5]="could not rotate backup directories"
[6]="partial backup detected"
[7]="rsync error"
[8]="invalid command line"
[9]="missing configuration file"
[10]="missing destination directory"
[11]="cannot aquire lock"
[12]="cannot determine PID of locked directory"
)
############################################################################### ###############################################################################
######################### helper functions ######################### helper functions
@@ -199,8 +217,8 @@ man() {
} }
usage () { usage () {
printf "usage: %s [-a PERIOD][-DfmnruvzZ] config-file\n" "$CMDNAME" printf "usage: %s [-a PERIOD][-DflmnruvzZ] config-file\n" "$CMDNAME"
exit 1 exit 8
} }
# log function # log function
@@ -245,33 +263,47 @@ echorun () {
# lock system # lock system
lock_lock() { lock_lock() {
local opid lock="$LOCKDIR/pid" local opid pidfile="$LOCKDIR/pid"
log -n "Setting lock: " #log -n "Setting lock: "
if [[ -r "$LOCKDIR" && -r "$lock" ]]; then log "Acquire lock (%s), pid=%d" "$LOCKDIR" "$PID"
read -r opid < "$lock" if [[ -d "$LOCKDIR" ]]; then
if ps -p "$opid" &> /dev/null; then if [[ -r "$pidfile" ]]; then
log "PID %d (in %s) still active. Exiting." "$opid" "$lock" read -r opid < "$pidfile"
exit 0 if ps -p "$opid" &> /dev/null; then
log "PID %d (in %s) still active. Exiting." "$opid" "$pidfile"
exit 11
fi
log "Stale lock file found (pid=%d), forcing unlock... " "$opid"
lock_unlock -f
log "Re-Acquire lock (%s), pid=%d" "$LOCKDIR" "$PID"
else
log "lockdir exists with unknown PID"
exit 12
fi fi
log "Stale lock file found (pid=%d), forcing unlock... " "$opid"
lock_unlock -f
fi fi
if ! mkdir "$LOCKDIR"; then if ! mkdir "$LOCKDIR"; then
log "Cannot create lock file. Exiting." log "Cannot create lock file. Exiting."
error_handler $LINENO 1 exit 4
fi fi
log "ok." printf "%d\n" "$PID" >> "$pidfile"
printf "%d\n" "$PID" >> "$lock"
LOCKED=y LOCKED=y
return 0 return 0
} }
lock_unlock() { lock_unlock() {
local force=n local force=n
[[ $# == 1 ]] && [[ $1 == -f ]] && force=y [[ $# == 1 && $1 == -f ]] && force=y
if [[ "$force" = y || "$LOCKED" = y ]]; then if [[ "$force" = y || "$LOCKED" = y ]]; then
rm --verbose "$LOCKDIR"/pid if [[ "$force" = y ]]; then
rm --dir --verbose "$LOCKDIR" log "Forced lock release (%s)" "$LOCKDIR"
else
log "Release lock (%s)" "$LOCKDIR"
fi
#rm --verbose "$LOCKDIR"/pid
#rm --dir --verbose "$LOCKDIR"
rm -vrf "$LOCKDIR"
else
log "Nothing to unlock (%s)" "$LOCKDIR"
fi fi
return 0 return 0
} }
@@ -279,20 +311,22 @@ lock_unlock() {
# Error handler.After these basic initializations, errors will be managed by the # Error handler.After these basic initializations, errors will be managed by the
# following handler. It is better to do this before the redirections below. # following handler. It is better to do this before the redirections below.
error_handler() { error_handler() {
ERROR=$2 local line="$1" err="$2"
echo "FATAL: Error line $1, exit code $2. Aborting." printf "FATAL: Error line %s, exit code %s. Aborting.\n" "$line" "$err"
exit "$ERROR" exit "$err"
} }
exit_handler() { exit_handler() {
local -i status="$?"
local error="${ERROR_STR[$status]}"
local subject="$CMDNAME: $SOURCEDIR on $HOSTNAME"
# we dont need lock file anymore (another backup could start from now). # we dont need lock file anymore (another backup could start from now).
log "exit_handler LOCKED=%s" "$LOCKED"
lock_unlock lock_unlock
if (( ERROR == 0 )); then if (( status == 0 )); then
SUBJECT="Successful $SUBJECT" subject="$subject (Success)"
else else
SUBJECT="Failure in $SUBJECT" subject="$subject (Failure: $error)"
fi fi
log -l -t "Ending backup." log -l -t "Ending backup."
@@ -300,8 +334,9 @@ exit_handler() {
if [[ $DEBUG = n ]]; then if [[ $DEBUG = n ]]; then
# restore stdout (not necessary), set temp file as stdin, close fd 3. # restore stdout (not necessary), set temp file as stdin, close fd 3.
# remove temp file (as still opened by stdin, will still be readable). # remove temp file (as still opened by stdin, will still be readable).
[[ $KEEPLOGFILE = y ]] && log "keeping log file: %s" "$LOGFILE"
exec 1<&3 3>&- 0<"$TMPFILE" exec 1<&3 3>&- 0<"$TMPFILE"
rm -f "$TMPFILE" [[ $KEEPLOGFILE = n ]] && rm -f "$TMPFILE"
else else
exec 0<<<"" # force empty input for the following exec 0<<<"" # force empty input for the following
fi fi
@@ -312,12 +347,8 @@ exit_handler() {
# more handled the final way. # more handled the final way.
{ {
# we write these logs here so that they are on top if no DEBUG. # we write these logs here so that they are on top if no DEBUG.
printf "%s: Exit code: %d " "$CMDNAME" "$ERROR" printf "%s: Exit code: %d (%s) " "$CMDNAME" "$status" \
if ((ERROR == 0)); then "${ERROR_STR[$status]}"
printf "(ok) "
else
printf "(error) "
fi
printf "in %d seconds (%d:%02d:%02d)\n\n" \ printf "in %d seconds (%d:%02d:%02d)\n\n" \
$((SECS)) $((SECS/3600)) $((SECS%3600/60)) $((SECS%60)) $((SECS)) $((SECS/3600)) $((SECS%3600/60)) $((SECS%60))
@@ -336,7 +367,7 @@ exit_handler() {
# email header # email header
printf "To: %s\n" "$MAILTO" printf "To: %s\n" "$MAILTO"
#printf "From: %s" "$MAILTO" #printf "From: %s" "$MAILTO"
printf "Subject: %s\n" "$SUBJECT" printf "Subject: %s\n" "$subject"
printf "MIME-Version: 1.0\n" printf "MIME-Version: 1.0\n"
printf 'Content-Type: multipart/mixed; boundary="%s"\n' "$MIMESTR" printf 'Content-Type: multipart/mixed; boundary="%s"\n' "$MIMESTR"
printf "\n" printf "\n"
@@ -385,7 +416,7 @@ exit_handler() {
parse_opts() { parse_opts() {
OPTIND=0 OPTIND=0
shopt -s extglob # to parse "-a" option shopt -s extglob # to parse "-a" option
while getopts a:DfmnruvzZ todo; do while getopts a:DflmnruvzZ todo; do
case "$todo" in case "$todo" in
a) a)
# we use US (Unit Separator, 0x1F, control-_) as separator # we use US (Unit Separator, 0x1F, control-_) as separator
@@ -409,6 +440,9 @@ parse_opts() {
r) r)
RESUME=y RESUME=y
;; ;;
l)
KEEPLOGFILE=y
;;
m) m)
man man
exit 0 exit 0
@@ -439,14 +473,14 @@ parse_opts() {
(( $# != 1 )) && usage (( $# != 1 )) && usage
CONFIG="$1" CONFIG="$1"
LOCKDIR=".sync-$SERVER-${CONFIG##*/}.lock"
if [[ ! -r "$CONFIG" ]]; then if [[ ! -r "$CONFIG" ]]; then
printf "%s: Cannot open $CONFIG file. Exiting.\n" "$CMDNAME" printf "%s: Cannot open $CONFIG file. Exiting.\n" "$CMDNAME"
exit 1 exit 9
fi fi
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$CONFIG" source "$CONFIG"
LOCKDIR="/tmp/$CMDNAME-$HOSTNAME-${CONFIG##*/}.lock"
} }
parse_opts "$@" parse_opts "$@"
@@ -479,14 +513,6 @@ if [[ $DEBUG = n ]]; then
exec 3<&1 >"$TMPFILE" # no more output on screen from now. exec 3<&1 >"$TMPFILE" # no more output on screen from now.
fi fi
exec 2>&1 exec 2>&1
if [[ ! -d "$SOURCEDIR" ]]; then
log -s "Invalid source directory (%s)." "$SOURCEDIR"
error_handler $LINENO 1
fi
if ! cd "$SOURCEDIR"; then
log -s "Cannot cd to %s." "$SOURCEDIR"
error_handler $LINENO 1
fi
# prepare list of backups, such as "daily 7 weekly 4", etc... # prepare list of backups, such as "daily 7 weekly 4", etc...
# the order is important. # the order is important.
@@ -499,7 +525,7 @@ TODO=()
log -l -t "Starting %s" "$CMDNAME" log -l -t "Starting %s" "$CMDNAME"
log "Bash version: %s.%s.%s" \ log "Bash version: %s.%s.%s" \
"${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" "${BASH_VERSINFO[2]}" "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" "${BASH_VERSINFO[2]}"
log "Hostname: %s" "$(hostname)" log "Hostname: %s" "$HOSTNAME"
log "Operating System: %s on %s" "$(uname -sr)" "$(uname -m)" log "Operating System: %s on %s" "$(uname -sr)" "$(uname -m)"
log "Config : %s\n" "$CONFIG" log "Config : %s\n" "$CONFIG"
log "Src dir: %s" "$SOURCEDIR" log "Src dir: %s" "$SOURCEDIR"
@@ -516,32 +542,42 @@ log -n "Compression: " && [[ $ZIPMAIL = gzip ]] && log "gzip" || log "none"
# check availability of necessary commands # check availability of necessary commands
declare -a cmdavail=() declare -a cmdavail=()
declare error=0
log -n "Checking for commands : " log -n "Checking for commands : "
for cmd in rsync base64 sendmail gzip; do for cmd in rsync base64 sendmail gzip; do
log -n "%s..." "$cmd" log -n "%s..." "$cmd"
if type -P "$cmd" > /dev/null; then if type -P "$cmd" > /dev/null; then
log -n "ok " log -n "ok "
else else
if [[ "$cmd" = "gzip" ]]; then (( error++ ))
log -n "NOK (compression disabled)"
ZIPMAIL="cat"
continue
fi
log -n "NOK " log -n "NOK "
case "$cmd" in
gzip)
log -n "(compression disabled) "
ZIPMAIL="cat"
(( error-- )) # Not an error
;;
sendmail)
MAILTO="" # to get some output in cron
;;
esac
cmdavail+=("$cmd") cmdavail+=("$cmd")
fi fi
done done
log "" log ""
if (( ${#cmdavail[@]} )); then (( ${#cmdavail[@]} )) && log -s "Please install the following programs: %s." \
log -s "Fatal. Please install the following programs: %s." "${cmdavail[*]}" "${cmdavail[*]}"
error_handler $LINENO 1 (( error > 0 )) && exit 2
fi
unset cmdavail unset cmdavail
unset error
# all logs from this point will be in email attachment
log -s "Mark" # to separate email body
log -l -t "Starting backup"
# create lock file # create lock file
lock_lock lock_lock
log -s "Mark" # to separate email body
# select handling depending on local or networked target (ssh or not). # select handling depending on local or networked target (ssh or not).
if [[ $SERVER = local ]]; then # local backup if [[ $SERVER = local ]]; then # local backup
@@ -574,7 +610,7 @@ rotate-files () {
# is to stop immediately instead of accepting strange side effects. # is to stop immediately instead of accepting strange side effects.
if $EXIST "${files[0]}" ; then if $EXIST "${files[0]}" ; then
log -s "Could not remove %s. This SHOULD NOT happen." "${files[0]}" log -s "Could not remove %s. This SHOULD NOT happen." "${files[0]}"
error_handler $LINENO $status exit 5
fi fi
fi fi
log "done." log "done."
@@ -593,6 +629,19 @@ rotate-files () {
return 0 return 0
} }
if [[ ! -d "$SOURCEDIR" ]]; then
log -s "Invalid source directory (%s)." "$SOURCEDIR"
exit 3
fi
if ! cd "$SOURCEDIR"; then
log -s "Cannot cd to %s." "$SOURCEDIR"
exit 3
fi
if ! $EXIST "$DESTDIR"; then
log -s 'destination directory (%s) missing.' "$DESTDIR"
exit 10
fi
# main loop. # main loop.
while [[ ${TODO[0]} != "" ]]; do while [[ ${TODO[0]} != "" ]]; do
# these variables to make the script easier to read. # these variables to make the script easier to read.
@@ -609,7 +658,7 @@ while [[ ${TODO[0]} != "" ]]; do
if $EXIST "$tdest"; then if $EXIST "$tdest"; then
if [[ $RESUME = n ]]; then if [[ $RESUME = n ]]; then
log -s '%s already exists, and no "resume" option.' "$tdest" log -s '%s already exists, and no "resume" option.' "$tdest"
error_handler $LINENO 1 exit 6
fi fi
log -s "Warning: Resuming %s partial backup." "$todo" log -s "Warning: Resuming %s partial backup." "$todo"
fi fi
@@ -641,7 +690,8 @@ while [[ ${TODO[0]} != "" ]]; do
"$DEST/daily-00" || status=$? "$DEST/daily-00" || status=$?
# error 24 is "vanished source file", and should be ignored. # error 24 is "vanished source file", and should be ignored.
if (( status != 24 && status != 0)); then if (( status != 24 && status != 0)); then
error_handler $LINENO $status log -s "rsync error %d" "$status"
exit 7
fi fi
aftersync # script to run after the sync aftersync # script to run after the sync
else # non-daily case. else # non-daily case.