Files
natelandau_ansible-homelab-…/templates/scripts/service_backups.sh.j2
Nathaniel Landau 1714dff877 feat: add gitea
2023-12-28 15:47:10 -05:00

1243 lines
40 KiB
Django/Jinja
Executable File

#!/usr/bin/env bash
_mainScript_() {
_setPATH_ "/bin" "/usr/bin" "/usr/local/bin"
if ! _rootAvailable_; then fatal "This script must be run as root"; fi
MULTIHOST=false
DESTDIR="{{ interpolated_nfs_service_storage }}/pi-cluster/backups/config_backups"
if [ -n "${JOB:-}" ]; then
JOBS_T0_BACKUP=("${JOB}")
else
JOBS_T0_BACKUP=(
influxdb
lidarr
prowlarr
radarr
readarr
sonarr
uptimekuma
gitea
)
fi
# Multihost jobs perform a backup based only if the files are present on the computer. It bypasses an allocation check
MULTI_HOST_JOBS=(
)
[ -z ${scriptName-} ] && scriptName="$(basename "$0")"
_validateNomadJobInfo_() {
# DESC: Validates that a job is running in Nomad AND that this script is being invoked
# on the same host that is running the allocation
# ARGS: $1 (Required) - Nomad job name
# OUTS: pass/fail
# USAGE: _validateNomadJobInfo_ "pihole"
# NOTE:
($purgeOnly) && return 0 # No need to validate that a job is running to purge old backups
local JOBTOVALIDATE="${1}"
if ${MULTIHOST}; then
if ! [[ $(nomad status -no-color ${MULTIHOSTJOB} | grep Status | head -n1 | awk '{print $3;}') == "running" ]]; then
debug "Specified job '${MULTIHOSTJOB}' has no running Nomad allocation. Continuing..."
return 1
fi
if [ -d "${SRCDIR}" ]; then
debug "Found ${SRCDIR}. Proceeding to backup..."
return 0
else
debug "'${SRCDIR}' not found on $(hostname). Continuing..."
return 1
fi
else
# TODO: Make this work when multiple allocations exist. Currently, it only takes the last allocation even if that is "Dead"
if ! [[ $(nomad status -no-color "${JOBTOVALIDATE}" | grep Status | head -n1 | awk '{print $3;}') == "running" ]]; then
debug "Specified job '${JOBTOVALIDATE}' has no running Nomad allocation. Continuing..."
return 1
fi
local ALLOC_ID=$(nomad status -no-color ${JOBTOVALIDATE} | tail -n 1 | awk '{print $1;}')
local ALLOC_HOST="$(nomad alloc status -no-color ${ALLOC_ID} | grep "Node Name" | awk '{print $4;}')"
if ! [[ "$(hostname)" == "${ALLOC_HOST}" ]]; then
debug "'$1' not running on $(hostname). Continuing..."
return 1
fi
return 0
fi
}
_determineFilesToBackup_() {
# DESC: Determines which files to backup based on the job name. Will
# default to "." (all files) if no specific job is specified
#
# ARGS:
# $1 (Required): Nomad job name
# OUTS:
# 0: Success
# 1: Failure
# stdout: Names of files to backup
# USAGE:
# _determineFilesToBackup_ "${JOB_NAME}"
[[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}"
case "${1}" in
lidarr) printf "%s" "lidarr.db config.xml" ;;
prowlarr) printf "%s" "prowlarr.db config.xml" ;;
radarr) printf "%s" "radarr.db config.xml" ;;
readarr) printf "%s" "readarr.db config.xml" ;;
sonarr) printf "%s" "sonarr.db config.xml" ;;
*) printf "%s" "." ;;
esac
}
_setRetention_() {
# DESC:
# Sets the retention policies for specific jobs
# ARGS:
# None
# OUTS:
# Provides values to BACKUP_RETENTION variables
local _jobName="${1}"
case "${_jobName}" in
influxdb)
BACKUP_RETENTION_HOURLY=1
BACKUP_RETENTION_DAILY=1
BACKUP_RETENTION_WEEKLY=1
BACKUP_RETENTION_MONTHLY=1
;;
lidarr)
BACKUP_RETENTION_HOURLY=2
BACKUP_RETENTION_DAILY=4
BACKUP_RETENTION_WEEKLY=2
BACKUP_RETENTION_MONTHLY=1
;;
uptimekuma)
BACKUP_RETENTION_HOURLY=1
BACKUP_RETENTION_DAILY=1
BACKUP_RETENTION_WEEKLY=1
BACKUP_RETENTION_MONTHLY=1
;;
*)
BACKUP_RETENTION_HOURLY=2
BACKUP_RETENTION_DAILY=6
BACKUP_RETENTION_WEEKLY=3
BACKUP_RETENTION_MONTHLY=4
;;
esac
}
_typeOfBackup_() {
# DESC: Determines if we are making a daily, weekly, or monthly backup
# ARGS: None
# OUTS: Outputs type of backup (daily, weekly, monthly, or hourly)
local DAYMONTH="$(date +%d)"
local DAYWEEK="$(date +%u)"
local TODAY="$(date +"%Y%m%d")"
if ls "${DESTDIR}" | grep --color=never "${JOBNAME}" | grep --color=never ${TODAY} &>/dev/null; then
echo "hourly"
elif [[ 10#${DAYMONTH} -eq 1 ]]; then
echo "monthly"
elif [[ 10#${DAYWEEK} -eq 7 ]]; then
echo "weekly"
elif [[ 10#${DAYWEEK} -lt 7 ]]; then
echo "daily"
fi
}
_doBackup_() {
# DESC: Creates a backup of a directory
# ARGS: $1 (Required) - Job name
# OUTS: pass/fail
# USAGE: _backupDirectory_ "[JOBJAME]"
# NOTE:
($purgeOnly) && return 0
if [[ $# -lt 1 ]]; then
fatal 'Missing required argument to _doBackup_()!' ${LINENO}
BACKUPSUCCESS=false
else
local JOB_TO_BACKUP="${1}"
fi
if [ ! -d "${SRCDIR}" ]; then
error "Could not find source directory: '${SRCDIR}'. Backup of '${JOB_TO_BACKUP}' aborted" ${LINENO}
BACKUPSUCCESS=false
return 1
fi
local BACKUPFILE="${JOB_TO_BACKUP}-$(date +"%Y%m%d-%H%M%S")-$(_typeOfBackup_).tgz"
local FILES_TO_BACKUP=$(_determineFilesToBackup_ "${JOB_TO_BACKUP}")
info "Running backup of ${JOB_TO_BACKUP}..."
# Don't run as root on macOS
# Notes on Tar Command
# - C - Change the current directory to specified dir before performing any operations
if [[ $(_detectOS_) == mac ]]; then
if [ $DRYRUN == true ]; then
dryrun "tar cz -C ${SRCDIR} ${FILES_TO_BACKUP} --file=${DESTDIR}/${BACKUPFILE}"
else
if tar cz -C ${SRCDIR} ${FILES_TO_BACKUP} --file=${DESTDIR}/${BACKUPFILE}; then
notice "Backup created: ${DESTDIR}/${BACKUPFILE}"
else
# If file created and larger than 1MB, then something was created, even
# if tar says it failed
if [ -n "$(find "${DESTDIR}/${BACKUPFILE}" -prune -size +1000000c)" ]; then
warning "Tar reported backup failed for '${JOB_TO_BACKUP}' but the file was created. Most likely something was changed while tar was reading it."
notice "Backup created: ${DESTDIR}/${BACKUPFILE}"
return 1
else
error "Backup of '${JOB_TO_BACKUP}' from srcdir ${SRCDIR} failed" ${LINENO}
BACKUPSUCCESS=false
return 1
fi
fi
fi
else # Not macOS
if [ $DRYRUN == true ]; then
dryrun "_runAsRoot_ tar cz -C ${SRCDIR} ${FILES_TO_BACKUP} --file=${DESTDIR}/${BACKUPFILE}"
else
if _runAsRoot_ tar cz -C ${SRCDIR} ${FILES_TO_BACKUP} --file=${DESTDIR}/${BACKUPFILE}; then
notice "Backup created: ${DESTDIR}/${BACKUPFILE}"
else
# If file created and larger than 1MB, then something was created, even
# if tar says it failed
if [ -n "$(find "${DESTDIR}/${BACKUPFILE}" -prune -size +1000000c)" ]; then
warning "Tar reported backup failed for '${JOB_TO_BACKUP}' but the file was created. Most likely something was changed while tar was reading it."
notice "Backup created: ${DESTDIR}/${BACKUPFILE}"
return 1
else
error "Backup of '${JOB_TO_BACKUP}' from srcdir ${SRCDIR} failed" ${LINENO}
BACKUPSUCCESS=false
return 1
fi
fi
fi
fi
BACKUPSUCCESS=true
return 0
}
_purgeOldBackups_() {
# DESC: Deletes the oldest files in the backup directory respecting backup retention
# ARGS: $1 (Required) - Job name
# OUTS: none
($noPurge) && return 0
($backupOnly) && return 0
if [[ $# -lt 1 ]]; then
fatal 'Missing required argument to _purgeOldBackups_()!' ${LINENO}
else
local JOBTOPURGE="${1}"
fi
[ -d "${SRCDIR}" ] || {
warning "Could not locate source directory: ${SRCDIR}. Continuing to next job..." ${LINENO}
return 1
}
debug "Checking old ${JOBTOPURGE} backups to purge..."
for BACKUPTYPE in daily weekly monthly hourly; do
if [[ ${BACKUPTYPE} == "hourly" ]]; then
local RETENTION=${BACKUP_RETENTION_HOURLY:-2}
elif [[ ${BACKUPTYPE} == "daily" ]]; then
local RETENTION=${BACKUP_RETENTION_DAILY:-6}
elif [[ ${BACKUPTYPE} == "weekly" ]]; then
local RETENTION=${BACKUP_RETENTION_WEEKLY:-3}
elif [[ ${BACKUPTYPE} == "monthly" ]]; then
local RETENTION=${BACKUP_RETENTION_MONTHLY:-2}
else
fatal "Could not set backup retention" ${LINENO}
fi
# Don't try to purge backups that don't exist....
if ls "${DESTDIR}" | grep ${JOBTOPURGE} | grep ${BACKUPTYPE} &>/dev/null; then
while read -r FILE_TO_DELETE; do
if [[ $(_detectOS_) == mac ]]; then
_execute_ -v "rm \"${DESTDIR}/${FILE_TO_DELETE}\"" "Deleted old backup: ${DESTDIR}/${FILE_TO_DELETE}"
else
_execute_ -v "_runAsRoot_ rm \"${DESTDIR}/${FILE_TO_DELETE}\"" "Deleted old backup: ${DESTDIR}/${FILE_TO_DELETE}"
fi
done < <(ls -t "${DESTDIR}" \
| grep --color=never ${JOBTOPURGE} \
| grep --color=never ${BACKUPTYPE} \
| sed -e 1,"${RETENTION}"d)
fi
done
}
_deleteJobFiles_() {
(${BACKUPSUCCESS}) || {
warning "Backup did not run successfully. Will not delete job files" ${LINENO}
return 0 # Only delete job files if backup completed successfully
}
debug "Attempting to delete job files from: ${SRCDIR}"
if [ -d "${SRCDIR}" ]; then
if [[ $(_detectOS_) == mac ]]; then
if ! _execute_ -v "rm -rf ${SRCDIR:?}/*" "Deleted job files from: ${SRCDIR}"; then
error "Could not delete source directory: ${SRCDIR}" ${LINENO}
return 1
fi
else
if ! _execute_ -v "_runAsRoot_ rm -rf ${SRCDIR:?}/*" "Deleted job files from: ${SRCDIR}"; then
error "Could not delete source directory: ${SRCDIR}" ${LINENO}
return 1
fi
fi
fi
return 0
}
########################################################################## Script Logic
(${DRYRUN}) || debug "====== Running script on $(hostname) ======"
for JOBNAME in "${JOBS_T0_BACKUP[@]}"; do
for MULTIHOSTJOB in "${MULTI_HOST_JOBS[@]}"; do
if [[ ${JOBNAME} =~ ${MULTIHOSTJOB} ]]; then
MULTIHOST=true
debug "Setting MULTIHOST to true for ${JOBNAME}" ${LINENO}
break
fi
done
BACKUPSUCCESS=false
SRCDIR="{{ interpolated_localfs_service_storage }}/${JOBNAME}"
debug "Working on backup job: ${JOBNAME}"
_setRetention_ "${JOBNAME}"
if _validateNomadJobInfo_ "${JOBNAME}"; then
if _doBackup_ "${JOBNAME}"; then
_purgeOldBackups_ "${JOBNAME}"
fi
else
(${ignoreAllocation}) && {
notice "${JOBNAME} - Post-stop task. Running without alloc check and deleting files"
if _doBackup_ "${JOBNAME}"; then
_purgeOldBackups_ "${JOBNAME}"
fi
}
if [ "${deleteDirectory}" == true ]; then
_deleteJobFiles_ # Only deletes files when job is not running on the host
fi
unset BACKUP_RETENTION_HOURLY
unset BACKUP_RETENTION_DAILY
unset BACKUP_RETENTION_WEEKLY
unset BACKUP_RETENTION_MONTHLY
fi
done
debug "script end"
}
# end _mainScript_
# ################################## Flags and defaults
# Required variables
QUIET=false
LOGLEVEL=ERROR
VERBOSE=false
FORCE=false
DRYRUN=false
declare -a ARGS=()
# Script specific
purgeOnly=false
ignoreAllocation=false
deleteDirectory=false
noPurge=false
backupOnly=false
BACKUP_RETENTION_HOURLY=2
BACKUP_RETENTION_DAILY=6
BACKUP_RETENTION_WEEKLY=3
BACKUP_RETENTION_MONTHLY=2
LOGFILE="{{ interpolated_nfs_service_storage }}/pi-cluster/logs/service_backups.log"
# ################################## Custom utility functions (Pasted from repository)
_execute_() {
# DESC:
# Executes commands while respecting global DRYRUN, VERBOSE, LOGGING, and QUIET flags
# ARGS:
# $1 (Required) - The command to be executed. Quotation marks MUST be escaped.
# $2 (Optional) - String to display after command is executed
# OPTS:
# -v Always print output from the execute function to STDOUT
# -n Use NOTICE level alerting (default is INFO)
# -p Pass a failed command with 'return 0'. This effectively bypasses set -e.
# -e Bypass _alert_ functions and use 'echo RESULT'
# -s Use '_alert_ success' for successful output. (default is 'info')
# -q Do not print output (QUIET mode)
# OUTS:
# stdout: Configurable output
# USE :
# _execute_ "cp -R \"~/dir/somefile.txt\" \"someNewFile.txt\"" "Optional message"
# _execute_ -sv "mkdir \"some/dir\""
# NOTE:
# If $DRYRUN=true, no commands are executed and the command that would have been executed
# is printed to STDOUT using dryrun level alerting
# If $VERBOSE=true, the command's native output is printed to stdout. This can be forced
# with '_execute_ -v'
local _localVerbose=false
local _passFailures=false
local _echoResult=false
local _echoSuccessResult=false
local _quietMode=false
local _echoNoticeResult=false
local opt
local OPTIND=1
while getopts ":vVpPeEsSqQnN" opt; do
case $opt in
v | V) _localVerbose=true ;;
p | P) _passFailures=true ;;
e | E) _echoResult=true ;;
s | S) _echoSuccessResult=true ;;
q | Q) _quietMode=true ;;
n | N) _echoNoticeResult=true ;;
*)
{
error "Unrecognized option '$1' passed to _execute_. Exiting."
_safeExit_
}
;;
esac
done
shift $((OPTIND - 1))
[[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}"
local _command="${1}"
local _executeMessage="${2:-$1}"
local _saveVerbose=${VERBOSE}
if "${_localVerbose}"; then
VERBOSE=true
fi
if "${DRYRUN}"; then
if "${_quietMode}"; then
VERBOSE=${_saveVerbose}
return 0
fi
if [ -n "${2:-}" ]; then
dryrun "${1} (${2})" "$(caller)"
else
dryrun "${1}" "$(caller)"
fi
elif ${VERBOSE}; then
if eval "${_command}"; then
if "${_quietMode}"; then
VERBOSE=${_saveVerbose}
elif "${_echoResult}"; then
printf "%s\n" "${_executeMessage}"
elif "${_echoSuccessResult}"; then
success "${_executeMessage}"
elif "${_echoNoticeResult}"; then
notice "${_executeMessage}"
else
info "${_executeMessage}"
fi
else
if "${_quietMode}"; then
VERBOSE=${_saveVerbose}
elif "${_echoResult}"; then
printf "%s\n" "warning: ${_executeMessage}"
else
warning "${_executeMessage}"
fi
VERBOSE=${_saveVerbose}
"${_passFailures}" && return 0 || return 1
fi
else
if eval "${_command}" >/dev/null 2>&1; then
if "${_quietMode}"; then
VERBOSE=${_saveVerbose}
elif "${_echoResult}"; then
printf "%s\n" "${_executeMessage}"
elif "${_echoSuccessResult}"; then
success "${_executeMessage}"
elif "${_echoNoticeResult}"; then
notice "${_executeMessage}"
else
info "${_executeMessage}"
fi
else
if "${_quietMode}"; then
VERBOSE=$_saveVerbose
elif "${_echoResult}"; then
printf "%s\n" "error: ${_executeMessage}"
else
warning "${_executeMessage}"
fi
VERBOSE=${_saveVerbose}
"${_passFailures}" && return 0 || return 1
fi
fi
VERBOSE=${_saveVerbose}
return 0
}
_rootAvailable_() {
# DESC:
# Validate we have superuser access as root (via sudo if requested)
# ARGS:
# $1 (optional): Set to any value to not attempt root access via sudo
# OUTS:
# 0 if true
# 1 if false
# CREDIT:
# https://github.com/ralish/bash-script-template
local _superuser
local _testEUID
if [[ ${EUID} -eq 0 ]]; then
_superuser=true
elif [[ -z ${1:-} ]]; then
debug 'Sudo: Updating cached credentials ...'
if sudo -v; then
if [[ $(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"') -eq 0 ]]; then
_superuser=true
else
_superuser=false
fi
else
_superuser=false
fi
fi
if [[ ${_superuser} == true ]]; then
debug 'Successfully acquired superuser credentials.'
return 0
else
debug 'Unable to acquire superuser credentials.'
return 1
fi
}
_runAsRoot_() {
# DESC:
# Run the requested command as root (via sudo if requested)
# ARGS:
# $1 (optional): Set to zero to not attempt execution via sudo
# $@ (required): Passed through for execution as root user
# OUTS:
# Runs the requested command as root
# CREDIT:
# https://github.com/ralish/bash-script-template
[[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}"
local _skip_sudo=false
if [[ ${1} =~ ^0$ ]]; then
_skip_sudo=true
shift
fi
if [[ ${EUID} -eq 0 ]]; then
"$@"
elif [[ -z ${_skip_sudo} ]]; then
sudo -H -- "$@"
else
fatal "Unable to run requested command as root: $*"
fi
}
_detectOS_() {
# DESC:
# Identify the OS the script is run on
# ARGS:
# None
# OUTS:
# 0 - Success
# 1 - Failed to detect OS
# stdout: One of 'mac', 'linux', 'windows'
# USAGE:
# _detectOS_
# CREDIT:
# https://github.com/labbots/bash-utility
local _uname
local _os
if _uname=$(command -v uname); then
case $("${_uname}" | tr '[:upper:]' '[:lower:]') in
linux*)
_os="linux"
;;
darwin*)
_os="mac"
;;
msys* | cygwin* | mingw* | nt | win*)
# or possible 'bash on windows'
_os="windows"
;;
*)
return 1
;;
esac
else
return 1
fi
printf "%s" "${_os}"
}
# ################################## Functions required for this template to work
_setColors_() {
# DESC:
# Sets colors use for alerts.
# ARGS:
# None
# OUTS:
# None
# USAGE:
# echo "${blue}Some text${reset}"
if tput setaf 1 >/dev/null 2>&1; then
bold=$(tput bold)
underline=$(tput smul)
reverse=$(tput rev)
reset=$(tput sgr0)
if [[ $(tput colors) -ge 256 ]] >/dev/null 2>&1; then
white=$(tput setaf 231)
blue=$(tput setaf 38)
yellow=$(tput setaf 11)
tan=$(tput setaf 3)
green=$(tput setaf 82)
red=$(tput setaf 1)
purple=$(tput setaf 171)
gray=$(tput setaf 250)
else
white=$(tput setaf 7)
blue=$(tput setaf 38)
yellow=$(tput setaf 3)
tan=$(tput setaf 3)
green=$(tput setaf 2)
red=$(tput setaf 1)
purple=$(tput setaf 13)
gray=$(tput setaf 7)
fi
else
bold="\033[4;37m"
reset="\033[0m"
underline="\033[4;37m"
reverse=""
white="\033[0;37m"
blue="\033[0;34m"
yellow="\033[0;33m"
tan="\033[0;33m"
green="\033[1;32m"
red="\033[0;31m"
purple="\033[0;35m"
gray="\033[0;37m"
fi
}
_alert_() {
# DESC:
# Controls all printing of messages to log files and stdout.
# ARGS:
# $1 (required) - The type of alert to print
# (success, header, notice, dryrun, debug, warning, error,
# fatal, info, input)
# $2 (required) - The message to be printed to stdout and/or a log file
# $3 (optional) - Pass '${LINENO}' to print the line number where the _alert_ was triggered
# OUTS:
# stdout: The message is printed to stdout
# log file: The message is printed to a log file
# USAGE:
# [_alertType] "[MESSAGE]" "${LINENO}"
# NOTES:
# - The colors of each alert type are set in this function
# - For specified alert types, the funcstac will be printed
local _color
local _alertType="${1}"
local _message="${2}"
local _line="${3:-}" # Optional line number
[[ $# -lt 2 ]] && fatal 'Missing required argument to _alert_'
if [[ -n ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then
_message="${_message} ${gray}(line: ${_line}) $(_printFuncStack_)"
elif [[ -n ${_line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then
_message="${_message} ${gray}(line: ${_line})"
elif [[ -z ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then
_message="${_message} ${gray}$(_printFuncStack_)"
fi
if [[ ${_alertType} =~ ^(error|fatal) ]]; then
_color="${bold}${red}"
elif [ "${_alertType}" == "info" ]; then
_color="${gray}"
elif [ "${_alertType}" == "warning" ]; then
_color="${red}"
elif [ "${_alertType}" == "success" ]; then
_color="${green}"
elif [ "${_alertType}" == "debug" ]; then
_color="${purple}"
elif [ "${_alertType}" == "header" ]; then
_color="${bold}${white}${underline}"
elif [ ${_alertType} == "notice" ]; then
_color="${bold}"
elif [ ${_alertType} == "input" ]; then
_color="${bold}${underline}"
elif [ "${_alertType}" = "dryrun" ]; then
_color="${blue}"
else
_color=""
fi
_writeToScreen_() {
("${QUIET}") && return 0 # Print to console when script is not 'quiet'
[[ ${VERBOSE} == false && ${_alertType} =~ ^(debug|verbose) ]] && return 0
if ! [[ -t 1 || -z ${TERM:-} ]]; then # Don't use colors on non-recognized terminals
_color=""
reset=""
fi
if [[ ${_alertType} == header ]]; then
printf "${_color}%s${reset}\n" "${_message}"
else
printf "${_color}[%7s] %s${reset}\n" "${_alertType}" "${_message}"
fi
}
_writeToScreen_
_writeToLog_() {
[[ ${_alertType} == "input" ]] && return 0
[[ ${LOGLEVEL} =~ (off|OFF|Off) ]] && return 0
if [ -z "${LOGFILE:-}" ]; then
LOGFILE="$(pwd)/$(basename "$0").log"
fi
[ ! -d "$(dirname "${LOGFILE}")" ] && mkdir -p "$(dirname "${LOGFILE}")"
[[ ! -f ${LOGFILE} ]] && touch "${LOGFILE}"
# Don't use colors in logs
local cleanmessage="$(echo "${_message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')"
# Print message to log file
printf "%s [%7s] %s %s\n" "$(date +"%b %d %R:%S")" "${_alertType}" "[$(/bin/hostname)]" "${cleanmessage}" >>"${LOGFILE}"
}
# Write specified log level data to logfile
case "${LOGLEVEL:-ERROR}" in
ALL | all | All)
_writeToLog_
;;
DEBUG | debug | Debug)
_writeToLog_
;;
INFO | info | Info)
if [[ ${_alertType} =~ ^(error|fatal|warning|info|notice|success) ]]; then
_writeToLog_
fi
;;
NOTICE | notice | Notice)
if [[ ${_alertType} =~ ^(error|fatal|warning|notice|success) ]]; then
_writeToLog_
fi
;;
WARN | warn | Warn)
if [[ ${_alertType} =~ ^(error|fatal|warning) ]]; then
_writeToLog_
fi
;;
ERROR | error | Error)
if [[ ${_alertType} =~ ^(error|fatal) ]]; then
_writeToLog_
fi
;;
FATAL | fatal | Fatal)
if [[ ${_alertType} =~ ^fatal ]]; then
_writeToLog_
fi
;;
OFF | off)
return 0
;;
*)
if [[ ${_alertType} =~ ^(error|fatal) ]]; then
_writeToLog_
fi
;;
esac
} # /_alert_
error() { _alert_ error "${1}" "${2:-}"; }
warning() { _alert_ warning "${1}" "${2:-}"; }
notice() { _alert_ notice "${1}" "${2:-}"; }
info() { _alert_ info "${1}" "${2:-}"; }
success() { _alert_ success "${1}" "${2:-}"; }
dryrun() { _alert_ dryrun "${1}" "${2:-}"; }
input() { _alert_ input "${1}" "${2:-}"; }
header() { _alert_ header "${1}" "${2:-}"; }
debug() { _alert_ debug "${1}" "${2:-}"; }
fatal() {
_alert_ fatal "${1}" "${2:-}"
_safeExit_ "1"
}
# shellcheck disable=SC1009,SC1054,SC1056,SC1072,SC1073,SC1083
{% raw %}
_printFuncStack_() {
# DESC:
# Prints the function stack in use. Used for debugging, and error reporting.
# ARGS:
# None
# OUTS:
# stdout: Prints [function]:[file]:[line]
# NOTE:
# Does not print functions from the alert class
local _i
_funcStackResponse=()
for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do
case "${FUNCNAME[$_i]}" in "_alert_" | "_trapCleanup_" | fatal | error | warning | notice | info | debug | dryrun | header | success) continue ;; esac
_funcStackResponse+=("${FUNCNAME[$_i]}:$(basename ${BASH_SOURCE[$_i]}):${BASH_LINENO[_i - 1]}")
done
printf "( "
printf %s "${_funcStackResponse[0]}"
printf ' < %s' "${_funcStackResponse[@]:1}"
printf ' )\n'
}
{% endraw %}
_safeExit_() {
# DESC:
# Cleanup and exit from a script
# ARGS:
# $1 (optional) - Exit code (defaults to 0)
# OUTS:
# None
if [[ -d ${SCRIPT_LOCK:-} ]]; then
if command rm -rf "${SCRIPT_LOCK}"; then
debug "Removing script lock"
else
warning "Script lock could not be removed. Try manually deleting ${tan}'${LOCK_DIR}'"
fi
fi
if [[ -n ${TMP_DIR:-} && -d ${TMP_DIR:-} ]]; then
if [[ ${1:-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then
command rm -r "${TMP_DIR}"
else
command rm -r "${TMP_DIR}"
debug "Removing temp directory"
fi
fi
trap - INT TERM EXIT
exit ${1:-0}
}
_trapCleanup_() {
# DESC:
# Log errors and cleanup from script when an error is trapped. Called by 'trap'
# ARGS:
# $1: Line number where error was trapped
# $2: Line number in function
# $3: Command executing at the time of the trap
# $4: Names of all shell functions currently in the execution call stack
# $5: Scriptname
# $6: $BASH_SOURCE
# USAGE:
# trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM
# OUTS:
# Exits script with error code 1
local _line=${1:-} # LINENO
local _linecallfunc=${2:-}
local _command="${3:-}"
local _funcstack="${4:-}"
local _script="${5:-}"
local _sourced="${6:-}"
if [[ "$(declare -f "fatal")" && "$(declare -f "_printFuncStack_")" ]]; then
_funcstack="'$(echo "${_funcstack}" | sed -E 's/ / < /g')'"
if [[ ${_script##*/} == "${_sourced##*/}" ]]; then
fatal "${7:-} command: '${_command}' (line: ${_line}) [func: $(_printFuncStack_)]"
else
fatal "${7:-} command: '${_command}' (func: ${_funcstack} called at line ${_linecallfunc} of '${_script##*/}') (line: ${_line} of '${_sourced##*/}') "
fi
else
printf "%s\n" "Fatal error trapped. Exiting..."
fi
if [ "$(declare -f "_safeExit_")" ]; then
_safeExit_ 1
else
exit 1
fi
}
_makeTempDir_() {
# DESC:
# Creates a temp directory to house temporary files
# ARGS:
# $1 (Optional) - First characters/word of directory name
# OUTS:
# Sets $TMP_DIR variable to the path of the temp directory
# USAGE:
# _makeTempDir_ "$(basename "$0")"
[ -d "${TMP_DIR:-}" ] && return 0
if [ -n "${1:-}" ]; then
TMP_DIR="${TMPDIR:-/tmp/}${1}.${RANDOM}.${RANDOM}.$$"
else
TMP_DIR="${TMPDIR:-/tmp/}$(basename "$0").${RANDOM}.${RANDOM}.${RANDOM}.$$"
fi
(umask 077 && mkdir "${TMP_DIR}") || {
fatal "Could not create temporary directory! Exiting."
}
debug "\$TMP_DIR=${TMP_DIR}"
}
_acquireScriptLock_() {
# DESC:
# Acquire script lock to prevent running the same script a second time before the
# first instance exits
# ARGS:
# $1 (optional) - Scope of script execution lock (system or user)
# OUTS:
# exports $SCRIPT_LOCK - Path to the directory indicating we have the script lock
# Exits script if lock cannot be acquired
# NOTE:
# If the lock was acquired it's automatically released in _safeExit_()
local _lockDir
if [[ ${1:-} == 'system' ]]; then
_lockDir="${TMPDIR:-/tmp/}$(basename "$0").lock"
else
_lockDir="${TMPDIR:-/tmp/}$(basename "$0").$UID.lock"
fi
if command mkdir "${LOCK_DIR}" 2>/dev/null; then
readonly SCRIPT_LOCK="${_lockDir}"
debug "Acquired script lock: ${yellow}${SCRIPT_LOCK}${purple}"
else
if [ "$(declare -f "_safeExit_")" ]; then
error "Unable to acquire script lock: ${tan}${LOCK_DIR}${red}"
fatal "If you trust the script isn't running, delete the lock dir"
else
printf "%s\n" "ERROR: Could not acquire script lock. If you trust the script isn't running, delete: ${LOCK_DIR}"
exit 1
fi
fi
}
_setPATH_() {
# DESC:
# Add directories to $PATH so script can find executables
# ARGS:
# $@ - One or more paths
# OUTS: Adds items to $PATH
# USAGE:
# _setPATH_ "/usr/local/bin" "${HOME}/bin" "$(npm bin)"
[[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}"
local _newPath
for _newPath in "$@"; do
if [ -d "${_newPath}" ]; then
if ! echo "${PATH}" | grep -Eq "(^|:)${_newPath}($|:)"; then
if PATH="${_newPath}:${PATH}"; then
debug "Added '${_newPath}' to PATH"
else
return 1
fi
else
debug "_setPATH_: '${_newPath}' already exists in PATH"
fi
else
debug "_setPATH_: can not find: ${_newPath}"
return 0
fi
done
return 0
}
_useGNUutils_() {
# DESC:
# Add GNU utilities to PATH to allow consistent use of sed/grep/tar/etc. on MacOS
# ARGS:
# None
# OUTS:
# 0 if successful
# 1 if unsuccessful
# PATH: Adds GNU utilities to the path
# USAGE:
# # if ! _useGNUUtils_; then exit 1; fi
# NOTES:
# GNU utilities can be added to MacOS using Homebrew
! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_"
if _setPATH_ \
"/usr/local/opt/gnu-tar/libexec/gnubin" \
"/usr/local/opt/coreutils/libexec/gnubin" \
"/usr/local/opt/gnu-sed/libexec/gnubin" \
"/usr/local/opt/grep/libexec/gnubin" \
"/usr/local/opt/findutils/libexec/gnubin" \
"/opt/homebrew/opt/findutils/libexec/gnubin" \
"/opt/homebrew/opt/gnu-sed/libexec/gnubin" \
"/opt/homebrew/opt/grep/libexec/gnubin" \
"/opt/homebrew/opt/coreutils/libexec/gnubin" \
"/opt/homebrew/opt/gnu-tar/libexec/gnubin"; then
return 0
else
return 1
fi
}
_homebrewPath_() {
# DESC:
# Add homebrew bin dir to PATH
# ARGS:
# None
# OUTS:
# 0 if successful
# 1 if unsuccessful
# PATH: Adds homebrew bin directory to PATH
# USAGE:
# # if ! _homebrewPath_; then exit 1; fi
! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_"
if _uname=$(command -v uname); then
if "${_uname}" | tr '[:upper:]' '[:lower:]' | grep -q 'darwin'; then
if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then
return 0
else
return 1
fi
fi
else
if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then
return 0
else
return 1
fi
fi
}
{% raw %}
_parseOptions_() {
# DESC:
# Iterates through options passed to script and sets variables. Will break -ab into -a -b
# when needed and --foo=bar into --foo bar
# ARGS:
# $@ from command line
# OUTS:
# Sets array 'ARGS' containing all arguments passed to script that were not parsed as options
# USAGE:
# _parseOptions_ "$@"
# Iterate over options
local _optstring=h
declare -a _options
local _c
local i
while (($#)); do
case $1 in
# If option is of type -ab
-[!-]?*)
# Loop over each character starting with the second
for ((i = 1; i < ${#1}; i++)); do
_c=${1:i:1}
_options+=("-${_c}") # Add current char to options
# If option takes a required argument, and it's not the last char make
# the rest of the string its argument
if [[ ${_optstring} == *"${_c}:"* && ${1:i+1} ]]; then
_options+=("${1:i+1}")
break
fi
done
;;
# If option is of type --foo=bar
--?*=*) _options+=("${1%%=*}" "${1#*=}") ;;
# add --endopts for --
--) _options+=(--endopts) ;;
# Otherwise, nothing special
*) _options+=("$1") ;;
esac
shift
done
set -- "${_options[@]:-}"
unset _options
# Read the options and set stuff
while [[ ${1:-} == -?* ]]; do
case $1 in
# Custom options
-j | --job)
shift
JOB="${1}"
;;
-a | --allocation) ignoreAllocation=true ;;
-d | --delete) deleteDirectory=true ;;
-p | --purge) purgeOnly=true ;;
-b | --backup) backupOnly=true ;;
-P | --noPurge) noPurge=true ;;
# Common options
-h | --help)
_usage_
_safeExit_
;;
--loglevel)
shift
LOGLEVEL=${1}
;;
--logfile)
shift
LOGFILE="${1}"
;;
-n | --dryrun) DRYRUN=true ;;
-v | --verbose) VERBOSE=true ;;
-q | --quiet) QUIET=true ;;
--force) FORCE=true ;;
--endopts)
shift
break
;;
*)
if [ "$(declare -f "_safeExit_")" ]; then
fatal "invalid option: $1"
else
printf "%s\n" "Invalid option: $1"
exit 1
fi
;;
esac
shift
done
if [[ -z ${*} || ${*} == null ]]; then
ARGS=()
else
ARGS+=("$@") # Store the remaining user input as arguments.
fi
}
{% endraw %}
_usage_() {
cat <<USAGE_TEXT
${bold}$(basename "$0") [OPTION]...${reset}
Performs a backup of files from a running Nomad allocation.
$ $(basename "$0") --allocation --delete --job sonarr
${bold}Options:${reset}
-a, --allocation Ignore the 'allocation running'. Used for post-stop tasks
-b, --backup Create a new backup only
-d, --delete Delete directory from disk after backing up
-j, --job Specify a specific job to backup
-p, --purge Purge old backups only
-P, --noPurge Do not purge old files
-h, --help Display this help and exit
--loglevel [LEVEL] One of: FATAL, ERROR, WARN, INFO, DEBUG, ALL, OFF (Default is 'ERROR')
--logfile [FILE] Full PATH to logfile. (Default is '${HOME}/logs/$(basename "$0").log')
-n, --dryrun Non-destructive. Makes no permanent changes.
-q, --quiet Quiet (no output)
-v, --verbose Output more information. (Items echoed to 'verbose')
--force Skip all user interaction. Implied 'Yes' to all actions.
USAGE_TEXT
}
# ################################## INITIALIZE AND RUN THE SCRIPT
# (Comment or uncomment the lines below to customize script behavior)
trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM
# Trap errors in subshells and functions
set -o errtrace
# Exit on error. Append '||true' if you expect an error
set -o errexit
# Use last non-zero exit code in a pipeline
set -o pipefail
# Confirm we have BASH greater than v4
[ "${BASH_VERSINFO:-0}" -ge 4 ] || {
printf "%s\n" "ERROR: BASH_VERSINFO is '${BASH_VERSINFO:-0}'. This script requires BASH v4 or greater."
exit 1
}
# Make `for f in *.txt` work when `*.txt` matches zero files
shopt -s nullglob globstar
# Set IFS to preferred implementation
IFS=$' \n\t'
# Run in debug mode
# set -o xtrace
# Initialize color constants
_setColors_
# Disallow expansion of unset variables
set -o nounset
# Force arguments when invoking the script
# [[ $# -eq 0 ]] && _parseOptions_ "-h"
# Parse arguments passed to script
_parseOptions_ "$@"
# Create a temp directory '$TMP_DIR'
# _makeTempDir_ "$(basename "$0")"
# Acquire script lock
# _acquireScriptLock_
# Add Homebrew bin directory to PATH (MacOS)
_homebrewPath_
# Source GNU utilities from Homebrew (MacOS)
_useGNUutils_
# Run the main logic script
_mainScript_
# Exit cleanly
_safeExit_