Skip to content

Instantly share code, notes, and snippets.

@PhrozenByte
Last active January 12, 2020 17:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PhrozenByte/a4c7cc7bc8335d1a34b333bf8013f160 to your computer and use it in GitHub Desktop.
Save PhrozenByte/a4c7cc7bc8335d1a34b333bf8013f160 to your computer and use it in GitHub Desktop.
Shell scripts to run Borg backups unattended

borg-unattended

Shell scripts to run Borg backups unattended.

Install

# backup script
mv cron-daily.borg-home /etc/cron.daily/borg-home
chmod +x /etc/cron.daily/borg-home

# weekly report script
mv cron-weekly.borg-report /etc/cron.weekly/borg-report
chmod +x /etc/cron.weekly/borg-report

# create backup log dir and report config and cache dirs 
mkdir /var/log/borg /etc/borg-report /var/cache/borg-report
chmod 750 /var/log/borg /var/cache/borg-report

# report config files
mv config.borg-home /etc/borg-report/borg-home

# logrotate config
mv logrotate.borg /etc/logrotate.d/borg
BORG_EXEC=( sudo -u "backup" -- borg )
BORG_REPO="/var/backups/borg-home"
#!/bin/bash
BORG_ID="home"
BORG_NAME="My /home dir"
BORG_EXEC=( sudo -u "backup" -- borg create "/var/backups/borg-home::{hostname}-{now:%Y-%m-%dT%H:%M:%S}" "/home" )
MAILTO="${MAILTO:-root}"
################################################################################
set -o pipefail
SUBJECT=""
OUTPUT=()
OUTPUT+=( "$(echo "$(date +'%F %T'): Starting Borg Backup \"$BORG_NAME\"..." \
| tee -a "/var/log/borg/$BORG_ID.log")" )
OUTPUT+=( "$("${BORG_EXEC[@]}" 2>&1 \
| tee -a "/var/log/borg/$BORG_ID.log")" )
BORG_STATUS=$?
if [ $BORG_STATUS -eq 0 ]; then
SUBJECT="\"$BORG_NAME\" was successful"
OUTPUT+=( "$(echo "$(date +'%F %T'): Your recent Borg Backup \"$BORG_NAME\" was successful. Yay!" \
| tee -a "/var/log/borg/$BORG_ID.log")" )
elif [ $BORG_STATUS -eq 1 ]; then
SUBJECT="\"$BORG_NAME\" finished with warnings"
OUTPUT+=( "$(echo "$(date +'%F %T'): Your recent Borg Backup \"$BORG_NAME\" finished with warnings." \
| tee -a "/var/log/borg/$BORG_ID.log")" )
else
SUBJECT="\"$BORG_NAME\" failed"
OUTPUT+=( "$(echo "$(date +'%F %T'): Your recent Borg Backup \"$BORG_NAME\" failed!" \
| tee -a "/var/log/borg/$BORG_ID.log")" )
fi
if [ $BORG_STATUS -ne 0 ]; then
printf '%s\n' "${OUTPUT[@]}" | mail -s "Borg <$USER@$(hostname)> $SUBJECT" "$MAILTO"
fi
exit $BORG_STATUS
#!/bin/bash
set -E -o pipefail
MAILTO="${MAILTO:-root}"
# prepare report
REPOS=()
REPOS_REPORT=()
ARCHIVES_REPORT=()
ARCHIVE_SIZE_SCRIPT=''
ARCHIVE_SIZE_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ '
ARCHIVE_SIZE_SCRIPT+='{ LENGTH=length($0); sub(/^ +/, ""); LENGTH-=length($0); getline; print substr($0, LENGTH + 1) }'
REPO_INFO_SCRIPT=''
REPO_INFO_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ '
REPO_INFO_SCRIPT+='{ exit } NR > 1 { print LINE } { LINE=$0 }'
REPO_SIZE_SCRIPT=''
REPO_SIZE_SCRIPT+='/^ +Original size +Compressed size +Deduplicated size$/ '
REPO_SIZE_SCRIPT+='{ PRINT=1 } PRINT { print }'
# handle archives files
# replace known archives file on success or
# delete temporary file on failure
ARCHIVES_FILES=()
ARCHIVES_FILES_NEW=()
function trap_failure {
local RC=$?
for ARCHIVES_FILE_NEW in "${ARCHIVES_FILES_NEW[@]}"; do
rm -f "$ARCHIVES_FILE_NEW"
done
trap - INT TERM ERR EXIT
[ $RC -ne 0 ] || RC=1
exit $RC
}
function trap_success {
INDEX=0
while [ $INDEX -lt ${#ARCHIVES_FILES[@]} ]; do
mv "${ARCHIVES_FILES_NEW[$INDEX]}" "${ARCHIVES_FILES[$INDEX]}"
((++INDEX))
done
trap - INT TERM ERR EXIT
exit 0
}
trap 'trap_failure' INT TERM ERR
trap 'trap_success' EXIT
# create reports
BORG_EXEC=( "borg" )
BORG_REPO=""
for REPO_FILE in /etc/borg-report/*; do
REPO_NAME="$(basename "$REPO_FILE")"
REPOS+=( "$REPO_NAME" )
# include repo file
. "$REPO_FILE"
[ -n "$BORG_REPO" ] || { echo "Invalid repo config file '$REPO_FILE': Invalid repository path" >&2; exit 1; }
# check known archives file
ARCHIVES_FILE="/var/cache/borg-report/$REPO_NAME"
if [ -e "$ARCHIVES_FILE" ] || [ -h "$ARCHIVES_FILE" ]; then
[ -f "$ARCHIVES_FILE" ] || { echo "Unable to read archives file '$ARCHIVES_FILE': No such file" >&2; exit 1; }
[ -r "$ARCHIVES_FILE" ] || { echo "Unable to read archives file '$ARCHIVES_FILE': Permission denied" >&2; exit 1; }
else
touch "$ARCHIVES_FILE"
fi
# get a list of not-yet-seen archives
ARCHIVES_FILE_NEW="$(mktemp)"
ARCHIVES_FILES+=( "$ARCHIVES_FILE" )
ARCHIVES_FILES_NEW+=( "$ARCHIVES_FILE_NEW" )
"${BORG_EXEC[@]}" list --short "$BORG_REPO" > "$ARCHIVES_FILE_NEW"
readarray -t ARCHIVES < <(
diff --new-line-format='%L' --old-line-format='' --unchanged-line-format='' \
"$ARCHIVES_FILE" "$ARCHIVES_FILE_NEW" \
|| true
)
LATEST_ARCHIVE="$(tail -n 1 "$ARCHIVES_FILE_NEW")"
# create archives report
ARCHIVE_SIZES=()
for ARCHIVE in "${ARCHIVES[@]}"; do
ARCHIVE_SIZE="$("${BORG_EXEC[@]}" info "$BORG_REPO::$ARCHIVE" | awk "$ARCHIVE_SIZE_SCRIPT")"
ARCHIVE_SIZES+=( "$(echo "$ARCHIVE_SIZE" | awk -F ' +' '{ print $2"|"$3"|"$4 }')|$ARCHIVE" )
done
ARCHIVES_REPORT+=( "$(printf "%s\n" "${ARCHIVE_SIZES[@]}")" )
# create repo report
LATEST_ARCHIVE_INFO="$("${BORG_EXEC[@]}" info "$BORG_REPO::$LATEST_ARCHIVE")"
REPO_REPORTS+=( "$(
printf -- '-%.0s' {1..80}; echo
awk "$REPO_INFO_SCRIPT" <<< "$LATEST_ARCHIVE_INFO"
printf -- '-%.0s' {1..80}; echo
awk "$REPO_SIZE_SCRIPT" <<< "$LATEST_ARCHIVE_INFO"
printf -- '-%.0s' {1..80}; echo
"${BORG_EXEC[@]}" list "$BORG_REPO"
)" )
done
# send notification email
SUBJECT="Borg <$(id -un)@$(hostname)> Activity Notification"
OUTPUT=()
OUTPUT+=( '<!DOCTYPE HTML>' )
OUTPUT+=( '<html lang="en">' )
OUTPUT+=( ' <head>' )
OUTPUT+=( ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' )
OUTPUT+=( ' <meta name="viewport" content="width=device-width">' )
OUTPUT+=( " <title>$SUBJECT</title>" )
OUTPUT+=( ' <style type="text/css">*{box-sizing:border-box;border:0;margin:0;padding:0}body,html{height:100%;background:#fff;font-family:Lucida Grande,Geneva,Verdana,sans-serif}#header,#main{width:100%;padding:0 .5em}#header{background:#428bca}.container{max-width:48em;margin:0 auto;padding:1em}#main .container{margin:4em auto;background:#f3f3f3}h1{padding:1.5em 0;color:#fff}h2{padding:0 0 .5em;margin:0 0 1em;border-bottom:1px solid #ccc;font-size:1.5rem}.content{padding:.5em 1em;background:#fff}.content pre,.content .table-wrapper{overflow-x:auto}table{border-spacing:0;min-width:100%}th{font-weight:bold;text-align:center;border-bottom:1px solid #ccc}td,th{padding:.25em .5em;background:#fff}td:first-child{white-space:nowrap;background:#f3f3f3}td:not(:first-child){min-width:9em;text-align:right}td:not(:last-child),th:not(:last-child){border-right:1px solid #ccc}tbody tr:last-child td:first-child{border-bottom-left-radius:5px}code{font-family:Lucida Console,Monaco,Courier New,monospace;font-size:.9em}h2 code{padding:.1em .2em;vertical-align:middle;background:#fff}#main .container,.content,h2 code,#main{border:1px solid #ccc;border-radius:5px}</style>' )
OUTPUT+=( ' </head>' )
OUTPUT+=( ' <body>' )
OUTPUT+=( ' <div id="header">' )
OUTPUT+=( ' <div class="container">' )
OUTPUT+=( ' <h1>Borg Activity Notification</h1>' )
OUTPUT+=( ' </div>' )
OUTPUT+=( ' </div>' )
OUTPUT+=( ' <div id="main">' )
for (( INDEX=0; INDEX < ${#REPOS[@]}; INDEX++ )); do
[ -n "${ARCHIVES_REPORT[$INDEX]}" ] || continue
OUTPUT+=( ' <div class="container">' )
OUTPUT+=( " <h2>Repository <code>${REPOS[$INDEX]}</code> - Recent Archives</h2>" )
OUTPUT+=( ' <div class="content">' )
OUTPUT+=( ' <div class="table-wrapper">' )
OUTPUT+=( ' <table>' )
OUTPUT+=( ' <thead>' )
OUTPUT+=( ' <tr>' )
OUTPUT+=( ' <th>Archive</th>' )
OUTPUT+=( ' <th>Original size</th>' )
OUTPUT+=( ' <th>Compressed size</th>' )
OUTPUT+=( ' <th>Deduplicated size</th>' )
OUTPUT+=( ' </tr>' )
OUTPUT+=( ' </thead>' )
OUTPUT+=( ' <tbody>' )
while IFS= read -r LINE; do
OUTPUT+=( ' <tr>' )
OUTPUT+=( " <td><code>$(echo "$LINE" | cut -d '|' -f 4-)</code></td>" )
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 1)</td>" )
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 2)</td>" )
OUTPUT+=( " <td>$(echo "$LINE" | cut -d '|' -f 3)</td>" )
OUTPUT+=( ' </tr>' )
done <<< "${ARCHIVES_REPORT[$INDEX]}"
OUTPUT+=( ' </tbody>' )
OUTPUT+=( ' </table>' )
OUTPUT+=( ' </div>' )
OUTPUT+=( ' </div>' )
OUTPUT+=( ' </div>' )
done
for (( INDEX=0; INDEX < ${#REPOS[@]}; INDEX++ )); do
OUTPUT+=( ' <div class="container">' )
OUTPUT+=( " <h2>Repository <code>${REPOS[$INDEX]}</code> - Overview</h2>" )
OUTPUT+=( ' <div class="content">' )
OUTPUT+=( " <pre><code>${REPO_REPORTS[$INDEX]}</code></pre>" )
OUTPUT+=( ' </div>' )
OUTPUT+=( ' </div>' )
done
OUTPUT+=( ' </div>' )
OUTPUT+=( ' </body>' )
OUTPUT+=( '</html>' )
printf '%s\n' "${OUTPUT[@]}" | mail -a "Content-Type: text/html; charset=utf-8" -s "$SUBJECT" "$MAILTO"
/var/log/borg/*.log {
weekly
rotate 4
compress
delaycompress
missingok
notifempty
copytruncate
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment