Created
February 10, 2018 04:55
-
-
Save pkutzner/999e309f97bf40ced35839794c2b5e56 to your computer and use it in GitHub Desktop.
Script for cron-able backups via restic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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