btrbk/contrib/cron/btrbk-mail

279 lines
7.8 KiB
Bash
Executable File

#!/bin/bash
## Wrapper script running "btrbk" and sending email with results
now=$(date +%Y%m%d)
declare -A rsync_src rsync_dst rsync_log rsync_rsh rsync_opt
declare -A sync_fs_onchange
##### start config section #####
# Email recipients, separated by whitespace:
mailto=${MAILTO:-root}
# Email subject:
mail_subject_prefix="btrbk <${HOSTNAME:-localhost}>"
# Add summary and/or detail (rsync/btrbk command output) to mail body.
# If both are not set, a mail is only sent on errors.
mail_summary=yes
mail_detail=no
# List of mountpoints to be mounted/unmounted (whitespace-separated)
# mount_targets="/mnt/btr_pool /mnt/backup"
mount_targets=
# rsync declarations (repeat complete block for more declarations):
rsync_src[example_data]="user@example.com:/data/"
rsync_dst[example_data]="/mnt/backup/example.com/data/"
rsync_log[example_data]="/mnt/backup/example.com/data-${now}.log"
rsync_rsh[example_data]="ssh -i /mnt/backup/ssh_keys/id_rsa"
rsync_opt[example_data]="-az --delete --inplace --numeric-ids --acls --xattrs"
# If set, add "rsync_dst" to "sync_fs" (see below) if rsync reports files transferred
#sync_fs_onchange[example_data]=yes
# Enable all rsync declarations (all indices of rsync_src array)
#rsync_enable=${!rsync_src[@]}
# Explicitly enable rsync declarations (whitespace-separated list)
#rsync_enable="example_data"
rsync_enable=
# If set, do not run btrbk if rsync reports no changes.
# If set to "quiet", do not send mail.
#skip_btrbk_if_unchanged=quiet
# Array of directories to sync(1) prior to running btrbk. This is
# useful for source subvolumes having "snapshot_create ondemand"
# configured in btrbk.conf.
#sync_fs=("/mnt/btr_data" "/mnt/btr_pool")
# btrbk command / options:
btrbk_command="run"
btrbk_opts="-c /etc/btrbk/btrbk.conf"
### Layout options:
# Prefix command output: useful when using mail clients displaying
# btrbk summary lines starting with ">>>" as quotations.
#mail_cmd_block_prefix='\\u200B' # zero-width whitespace
#mail_cmd_block_prefix=". "
# Newline character
BR=$'\n'
##### end config section #####
check_options()
{
[[ -n "$btrbk_command" ]] || die "btrbk_command is not set"
for key in $rsync_enable; do
[[ -n "${rsync_src[$key]}" ]] || die "rsync_src is not set for \"$key\""
[[ -n "${rsync_dst[$key]}" ]] || die "rsync_dst is not set for \"$key\""
[[ -n "${rsync_opt[$key]}" ]] || die "rsync_opt is not set for \"$key\""
done
}
send_mail()
{
# assemble mail subject
local subject="$mail_subject_prefix"
[[ -n "$has_errors" ]] && subject+=" ERROR";
[[ -n "$status" ]] && subject+=" - $status";
[[ -n "$xstatus" ]] && subject+=" (${xstatus:2})";
# assemble mail body
local body=
if [[ -n "$info" ]] && [[ -n "$has_errors" ]] || [[ "${mail_summary:-no}" = "yes" ]]; then
body+="$info"
fi
if [[ -n "$detail" ]] && [[ -n "$has_errors" ]] || [[ "${mail_detail:-no}" = "yes" ]]; then
[[ -n "$body" ]] && body+="${BR}${BR}DETAIL:${BR}"
body+="$detail"
fi
# skip sending mail on empty body
if [[ -z "$body" ]] && [[ -n "$has_errors" ]]; then
body+="FATAL: something went wrong (errors present but empty mail body)${BR}"
fi
[[ -z "$body" ]] && exit 0
# send mail
echo "$body" | mail -s "$subject" $mailto
if [[ $? -ne 0 ]]; then
echo "$0: Failed to send btrbk mail to \"$mailto\", dumping mail:${BR}" 1>&2
echo "<mail_subject>$subject</mail_subject>${BR}<mail_body>${BR}$body</mail_body>" 1>&2
fi
}
einfo()
{
info+="$1${BR}"
}
ebegin()
{
ebtext=$1
detail+="${BR}### $1${BR}"
}
eend()
{
if [[ $1 -eq 0 ]]; then
eetext=${3-success}
detail+="${BR}"
else
has_errors=1
eetext="ERROR (code=$1)"
[[ -n "$2" ]] && eetext+=": $2"
detail+="${BR}### $eetext${BR}"
fi
info+="$ebtext: $eetext${BR}"
return $1
}
die()
{
einfo "FATAL: ${1}, exiting"
has_errors=1
send_mail
exit 1
}
run_cmd()
{
cmd_out=$("$@" 2>&1)
local ret=$?
detail+="++ ${@@Q}${BR}"
if [[ -n "${mail_cmd_block_prefix:-}" ]] && [[ -n "$cmd_out" ]]; then
detail+=$(echo -n "$cmd_out" | sed "s/^/${mail_cmd_block_prefix}/")
detail+="${BR}"
else
detail+=$cmd_out
fi
return $ret
}
mount_all()
{
# mount all mountpoints listed in $mount_targets
mounted=""
for mountpoint in $mount_targets; do
ebegin "Mounting $mountpoint"
run_cmd findmnt -n $mountpoint
if [[ $? -eq 0 ]]; then
eend -1 "already mounted"
else
detail+="${BR}"
run_cmd mount --target $mountpoint
eend $? && mounted+=" $mountpoint"
fi
done
}
umount_mounted()
{
for mountpoint in $mounted; do
ebegin "Unmounting $mountpoint"
run_cmd umount $mountpoint
eend $?
done
}
check_options
mount_all
#
# run rsync for all $rsync_enable
#
for key in $rsync_enable; do
ebegin "Running rsync[$key]"
if [[ -d "${rsync_dst[$key]}" ]]; then
# There is no proper way to get a proper machine readable
# output of "rsync did not touch anything at destination", so
# we add "--info=stats2" and parse the output.
# NOTE: This also appends the stats to the log file (rsync_log).
# Another approach to count the files would be something like:
# "rsync --out-format='' | wc -l"
run_cmd rsync ${rsync_opt[$key]} \
--info=stats2 \
${rsync_log[$key]:+--log-file="${rsync_log[$key]}"} \
${rsync_rsh[$key]:+-e "${rsync_rsh[$key]}"} \
"${rsync_src[$key]}" \
"${rsync_dst[$key]}"
exitcode=$?
# parse stats2 (count created/deleted/transferred files)
REGEXP=$'\n''Number of created files: ([0-9]+)'
REGEXP+='.*'$'\n''Number of deleted files: ([0-9]+)'
REGEXP+='.*'$'\n''Number of regular files transferred: ([0-9]+)'
if [[ $cmd_out =~ $REGEXP ]]; then
rsync_stats="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/${BASH_REMATCH[3]}"
rsync_stats_long="${BASH_REMATCH[1]} created, ${BASH_REMATCH[2]} deleted, ${BASH_REMATCH[3]} transferred"
nfiles=$(( ${BASH_REMATCH[1]} + ${BASH_REMATCH[2]} + ${BASH_REMATCH[3]} ))
else
rsync_stats_long="failed to parse stats, assuming files transferred"
rsync_stats="-1/-1/-1"
nfiles=-1
fi
eend $exitcode "$rsync_stats_long" "$rsync_stats_long"
xstatus+=", rsync[$key]=$rsync_stats"
if [[ $nfiles -ne 0 ]]; then
# NOTE: on error, we assume files are transferred
rsync_files_transferred=1
[[ -n "${sync_fs_onchange[$key]}" ]] && sync_fs+=("${rsync_dst[$key]}")
fi
else
eend -1 "Destination directory not found, skipping: ${rsync_dst[$key]}"
fi
done
# honor skip_btrbk_if_unchanged (only if rsync is enabled and no files were transferred)
if [[ -n "$rsync_enable" ]] && [[ -n "$skip_btrbk_if_unchanged" ]] && [[ -z "$rsync_files_transferred" ]]; then
einfo "No files transferred, exiting"
status="No files transferred"
umount_mounted
if [[ "$skip_btrbk_if_unchanged" != "quiet" ]] || [[ -n "$has_errors" ]]; then
send_mail
fi
exit 0
fi
#
# sync filesystems in sync_fs
#
if [[ ${#sync_fs[@]} -gt 0 ]]; then
ebegin "Syncing filesystems at ${sync_fs[@]}"
run_cmd sync -f "${sync_fs[@]}"
eend $?
fi
#
# run btrbk
#
ebegin "Running btrbk"
run_cmd btrbk ${btrbk_opts:-} ${btrbk_command}
exitcode=$?
case $exitcode in
0) status="All backups successful"
;;
3) status="Another instance of btrbk is running, no backup tasks performed!"
;;
10) status="At least one backup task aborted!"
;;
*) status="btrbk failed with error code $exitcode"
;;
esac
eend $exitcode "$status"
umount_mounted
send_mail