#!/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[@]} # Explicitely 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 "$subject${BR}${BR}$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