#!/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=( sonarr radarr influxdb lidarr prowlarr uptimekuma ) 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" ;; sonarr) printf "%s" "sonarr.db config.xml" ;; radarr) printf "%s" "radarr.db config.xml" ;; prowlarr) printf "%s" "prowlarr.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 ;; *) BACKUP_RETENTION_HOURLY=2 BACKUP_RETENTION_DAILY=6 BACKUP_RETENTION_WEEKLY=3 BACKUP_RETENTION_MONTHLY=2 ;; 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 <