Skip to content

Instantly share code, notes, and snippets.

@pkutzner
Created February 10, 2018 04:55
Show Gist options
  • Save pkutzner/999e309f97bf40ced35839794c2b5e56 to your computer and use it in GitHub Desktop.
Save pkutzner/999e309f97bf40ced35839794c2b5e56 to your computer and use it in GitHub Desktop.
Script for cron-able backups via restic
#!/usr/bin/env bash
# Fail early if we weren't given enough parameters
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <path_to_env_vars> <action> [debug]"
exit 1
fi
## === DEFAULTS === ##
RESTICPATH="/opt/restic"
ENV_FILES_PATH="/etc/restic"
LOG_FILE="/var/log/resticbackup.log"
ERR_FILE="/var/log/resticbackup.err"
# Defaults, these can be overridden in ENV_FILE
MONTHLY_SNAPSHOTS_TO_REMEMBER=12
WEEKLY_SNAPSHOTS_TO_REMEMBER=4
DAILY_SNAPSHOTS_TO_REMEMBER=30
# Environment variables to be set and used by this script.
# These variables MUST be defined in $ENV_FILE
#
# Replace B2_* with appropriate environment variables
# based on your cloud storage provider below.
#
# NOTE: Must match what is declared in ENV_FILE
#
# Backblaze B2:
# - B2_ACCOUNT_ID
# - B2_ACCOUNT_KEY
#
# Amazon S3:
# - AWS_ACCESS_KEY_ID
# - AWS_SECRET_ACCESS_KEY
#
# MS Azure Blob Storage:
# - AZURE_ACCOUNT_NAME
# - AZURE_ACCOUNT_KEY
#
# Google Cloud Storage:
# - GOOGLE_PROJECT_ID
# - GOOGLE_APPLICATION_CREDENTIALS
ENV_VARS=(
'B2_ACCOUNT_ID'
'B2_ACCOUNT_KEY'
'RESTIC_REPOSITORY'
'RESTIC_PASSWORD_FILE'
)
## === END DEFAULTS === ##
########################################
##### DO NOT EDIT BELOW THIS POINT #####
########################################
# Check if we were given an absolute path to our env file.
# If we weren't, prepend the ENV_FILES_PATH to the filename.
if [[ $1 =~ ^[/~][:print:]* ]]; then
ENV_FILE="$1"
else
ENV_FILE="${ENV_FILES_PATH}/$1"
fi
LOCKFILE="/var/lock/`basename $0`"
LOCKFD=99
RESTIC_CMD=( "$RESTICPATH/restic" )
# Day of week to perform weekly backups.
# (0=Sun, 1=Mon .. 6=Sat)
DOWEEKLY="6"
_init() {
# Current day of week (0=Sun, 6=Sat)
DOW=$(date +%u)
# Day of month, don't pad with leading 0
DOM=$(date +%-d)
}
_lock() {
flock -$1 $LOCKFD
}
# Turtles all the way down.
_iscli(){
# Equivalent of doing a `basename $0` without the subshell.
scriptname="${0##*/}"
shells="^[ba|z|k|s]?sh|^init|^screen|^su|^tmux|${scriptname}"
# If we were passed a PID, use it, otherwise use the PID
# of the parent script.
pid=${1:-$$}
# Get process name of pid
pidname=`ps --no-heading -o %c -p ${pid}`
# Get process information for pid
stat=($(</proc/${pid}/stat))
# Get the ppid (parent pid) of pid
ppid=${stat[3]}
# Get the process name of ppid
ppidname="`ps --no-heading -o %c -p ${ppid}`"
# Check to see if the ppid name matches the regex in $shells
isclitest="`echo "${ppidname}" | grep -iv -e "${shells}"`"
# Recursively check our process tree to see if we were run
# from an interactive shell at some point.
until [ "${ppid}" -eq "1" ] || [ "${iscli}" = "1" ]; do
if [[ -n "${isclitest}" ]]; then
iscli="1"
else
iscli="0"
_iscli ${ppid}
fi
done
}
_start_logging() {
exec 6>&1
exec 7>&2
if ! exec > >($SUDO tee -a "$LOG_FILE" >/dev/null); then
exec 1>&6 6>&-
exec 2>&7 7>&-
echo >&2 "[ERROR]: Unable to set up logging."
exit 1
elif ! exec 2> >($SUDO tee -a "$ERR_FILE" 2>/dev/null); then
exec 1>&6 6>&-
exec 2>&7 7>&-
echo >&2 "[ERROR]: Unable to set up logging."
exit 1
fi
}
_stop_logging() {
exec 1>&6 6>&-
exec 2>&7 7>&-
}
_cleanup() {
if [[ "$LOGGING" =~ yes ]]; then
_stop_logging
fi
_no_more_locking
kill $( pgrep -P $$ | tr "\n" " " ) 2>/dev/null
}
_no_more_locking() {
_lock u
_lock xn && rm -f $LOCKFILE
}
_prepare_locking() {
eval "exec $LOCKFD>\"$LOCKFILE\""
trap _cleanup EXIT
# Trap INT and TERM signals and kill all children before committing suicide.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM
}
# Handy way of running multiple bash checks against a file in one go.
_check_file() {
local FLAGS=$1
local PATH=$2
if [ -z "$PATH" ]; then
if [ -z "$FLAGS" ]; then
echo "${FUNCNAME[0]} - must specify at least a path" >&2
exit 1
fi
PATH=$FLAGS
FLAGS=-e
fi
FLAGS=${FLAGS#-}
while [ -n "$FLAGS" ]; do
local FLAG=`printf "%c" "$FLAGS"`
if [ ! -$FLAG $PATH ]; then false; return; fi
FLAGS=${FLAGS#?}
done
true
}
_prepare_locking
exlock_now() {
# Obtain an exclusive lock or immediately fail
_lock xn
}
exlock() {
# Obtain an exclusive lock
_lock x
}
shlock() {
# Obtain a shared lock
_lock s
}
unlock() {
# Drop a lock
_lock u
}
checkroot() {
local answer
if (( $EUID != 0 )); then
echo "$(basename $0) needs to run as root."
echo "Press ENTER or wait 10 seconds for default."
read -t 10 -p "Acquire root permissions now? [y/N]: " answer
if [[ "${answer:-n}" =~ ^(Y|y)$ ]]; then
# Keep-alive: update existing 'sudo' time stamp until script has finished.
# The default sudo timeout is 15 minutes, this runs sudo -v (validate) every 14 min.
while true; do /usr/bin/sudo -v; sleep 840; done 2>/dev/null &
# Script was run as normal user, and therefore commands need to be run with sudo.
# '2' is the return value we use to let other parts of the script know this is the
# case.
return 2
else
# We aren't root, and user declined privilege elevation, so we can't go further.
exit 1
fi
fi
# We were run as root already. The below is not needed, but added for clarity.
return 0
}
checkrepo() {
# Check if the supplied repository already exists. If not, prompt the user
# if they want to create it. Defaults to 'yes' after 10 seconds, in the case
# we were run non-interactively.
local answer
${SUDO[@]} ${RESTIC_CMD[@]} snapshots >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Specified repo $REPOSITORY doesn't seem to exist!"
echo "Press ENTER or wait 10 seconds for default."
read -t 10 -p "Create now? [Y/n]" answer
if [[ "${answer:-y}" =~ ^(Y|y)$ ]]; then
echo "Creating repository..."
$ECHO /usr/bin/nice -n 19 /usr/bin/ionice -c2 -n7 ${SUDO[@]} ${RESTIC_CMD[@]} init
else
echo "Repository not created, exiting!"
exit 1
fi
else
echo "Specified repo $RESTIC_REPOSITORY exists... continuing."
fi
}
restic() {
if [[ ! -z $ULIMIT ]] && [[ $ULIMIT =~ ^[0-9]+$ ]]; then
local ulimit="--limit-upload $ULIMIT"
fi
case "$1" in
[Bb]ackup)
for d in "${BACKUPDIRS[@]}"; do
$ECHO /usr/bin/nice -n 19 /usr/bin/ionice -c2 -n7 ${SUDO[@]} ${RESTIC_CMD[@]} ${ulimit} --cache-dir /var/cache/restic backup "$d"
done
;;
[Cc]lean)
$ECHO /usr/bin/nice -n 19 /usr/bin/ionice -c2 -n7 ${SUDO[@]} ${RESTIC_CMD[@]} forget --prune -l ${SNAPSHOTS_TO_REMEMBER}
;;
[Uu]nlock)
$ECHO ${SUDO[@]} ${RESTIC_CMD[@]} unlock
;;
[Cc]heck)
$ECHO ${SUDO[@]} ${RESTIC_CMD[@]} check
(( $? != 0 )) && { echo "Integrity check for $REPOSITORY failed!"; exit 1; }
;;
*)
echo "Unknown action $1"; exit 1
;;
esac
}
# Alright, we're starting.
# Attempt to get an exclusive lock on our lock file or exit with error.
# This will prevent running again if the last invocation has not finished.
exlock_now || exit 1
# Were we invoked with 'debug'? If so, populate ECHO so commands are just
# echoed to screen instead of being executed.
if [[ ! -z $3 ]] && [[ "$3" =~ ^(debug|Debug|DEBUG)$ ]]; then
ECHO="echo"
fi
# Initialize date variables.
_init
# Check if we've effectively been run as root
checkroot
# checkroot returns a number, 0 or 2, capture it here.
# (0 = we were run as root, 2 = we were run as a luser)
USERMODE=$?
# We're running as a non-privileged user, set the sudo binary.
(( $USERMODE == 2 )) && SUDO=("/usr/bin/sudo")
# Were we run from an interactive shell? If not, log to file
# and quiet down restic.
if ! _iscli; then
RESTIC_CMD+=("-q")
LOGGING="yes"
_start_logging
fi
# Check if our environment file exists and, if so, source it.
# 'source' expects a filename, but we're using output redirection
# to feed it to account for sudo, so use /dev/stdin as the file.
if _check_file -efs "${ENV_FILE}"; then
# Referencing an array variable as a normal variable only
# gives you the first item in the array; in this case
# the full path to the sudo binary.
source /dev/stdin < <($SUDO cat ${ENV_FILE})
else
echo "File $ENV_FILE doesn't exist, is not a 'regular' file, or is empty! Exiting."
exit 1
fi
# Is it the first day of the month? If so, do a monthly backup.
if [[ $DOM -eq 1 ]]; then
for i in "${ENV_VARS[@]}"; do
if [[ "$i" = "RESTIC_REPOSITORY" ]]; then
eval REPOSITORY="\$${i}/monthly"
if [[ $USERMODE = 2 ]]; then
eval SUDO+=( "$i=\"${REPOSITORY}\"" )
else
eval export $i="${REPOSITORY}"
fi
else
if [[ $USERMODE = 2 ]]; then
eval SUDO+=( "$i=\"\$$i\"" )
else
eval export $i
fi
fi
done
checkrepo
SNAPSHOTS_TO_REMEMBER=${MONTLY_SNAPSHOTS_TO_REMEMBER} restic "$2"
fi
# Is it our specified day to run weekly backups? If so, do a weekly backup.
if [[ $DOW -eq $DOWEEKLY ]]; then
for i in "${ENV_VARS[@]}"; do
if [[ "$i" = "RESTIC_REPOSITORY" ]]; then
eval REPOSITORY="\$${i}/weekly"
if [[ $USERMODE = 2 ]]; then
eval SUDO+=( "$i=\"${REPOSITORY}\"" )
else
eval export $i="${REPOSITORY}"
fi
else
if [[ $USERMODE = 2 ]]; then
eval SUDO+=( "$i=\"\$$i\"" )
else
eval export $i
fi
fi
done
checkrepo
SNAPSHOTS_TO_REMEMBER=${WEEKLY_SNAPSHOTS_TO_REMEMBER} restic "$2"
fi
for i in "${ENV_VARS[@]}"; do
if [[ "$i" = "RESTIC_REPOSITORY" ]]; then
eval REPOSITORY="\$${i}/daily"
if [[ $USERMODE -eq 2 ]]; then
eval SUDO+=( "$i=\"${REPOSITORY}\"" )
else
eval export $i="${REPOSITORY}"
fi
else
if [[ $USERMODE -eq 2 ]]; then
eval SUDO+=( "$i=\"\$$i\"" )
else
eval export $i
fi
fi
done
# Always do a daily backup.
checkrepo
SNAPSHOTS_TO_REMEMBER=${DAILY_SNAPSHOTS_TO_REMEMBER} restic "$2"
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment