diff --git a/pilerbackup.details b/pilerbackup.details new file mode 100644 index 0000000..dc19ca7 --- /dev/null +++ b/pilerbackup.details @@ -0,0 +1,54 @@ +####### +### Piler backup script configuration details +### +### This file contains sensitive information, make sure you have protected +### it by restricting permissions! +### Run the following in the directory where this file is located: +### chown root:root ./seafbackup.details +### chmod 600 ./seafbackup.details +### +### Do NOT include any commands in this file as they WILL be executed!!! +####### + + +### borg details +# if you're unsure what to enter here, please consult the repo wiki and/or +# the borg documentation + +# base configuration directory for borg, all borg parameters use this directory +# as their 'root'. I recommended setups with this being "/var/borgbackup", the +# default is "$HOME" or "~$USER" in that order. If you're unsure, try "$HOME" +borgBaseDir="/var/borgbackup" + +# full path to the SSH key used to connect to your remote backup server +borgSSHKey="/var/borgbackup/private.key" + +# connection string to access the borg repo on your remote backup server +# this is usually in the form user@servername.tld:repoName/ +borgConnectRepo="jdoe123@borg.server.net:seafileBackup/" + +# password to access repo +# this was set when the repo was initialized and, while optional, is HIGHLY +# recommended for security +borgRepoPassphrase="p@ssW0rd" + +# OPTIONAL: path to text file containing a list (one per line) of files/ +# directories to include in your backup along with Seafile data +# see repo wiki for more details +# leave blank if you only want to backup Seafile related files/directories. +borgXtraListPath="/root/seafileBackup/xtraLocations.borg" + +# OPTIONAL: path to file containing files/directories or 'patterns' to be +# excluded in a BORG RECOGNIZED format +# see repo wiki for more details or consult borg documentation +# leave blank for no exclusions. +borgExcludeListPath="/root/seafileBackup/excludeLocations.borg" + +# parameters to determine how borg deletes aged backups +# more details in the repo wiki and/or borg documentation +# leave blank to skip pruning altogether -- NOT recommended! +borgPruneSettings="--keep-within=7d --keep-daily=30 --keep-weekly=12 --keep-monthly=-1" + +# location of borg instance on your remote backup server +# this is very often just "borg1" +borgRemote="borg1" diff --git a/pilerbackup.sh b/pilerbackup.sh new file mode 100644 index 0000000..f76844c --- /dev/null +++ b/pilerbackup.sh @@ -0,0 +1,584 @@ +#!/bin/sh + +####### +### Backup exported email from piler (bare-metal) +####### + + +### text formatting presents +norm=$(tput sgr0) +err=$(tput bold)$(tput setaf 1) +warn=$(tput bold)$(tput setaf 3) +ok=$(tput setaf 2) +yellow=$(tput setaf 3) +cyan=$(tput setaf 6) +magenta=$(tput setaf 5) + + +### trap +trap trapExit 1 2 3 6 + + +### functions + +# bad configuration value passed in details file +badDetails () { + if [ "$1" = "empty" ]; then + exitError 130 "details:${2} cannot be NULL (undefined)" + elif [ "$1" = "dne" ]; then + exitError 131 "details:${2} file or directory does not exist." + fi +} + +# bad parameter passed to script +badParam () { + if [ "$1" = "dne" ]; then + printf "${err}\nError: '%s %s'\n" "$2" "$3" + printf "file or directory does not exist.${norm}\n\n" + exit 1 + elif [ "$1" = "empty" ]; then + printf "${err}\nError: '%s' cannot have a NULL (empty) value.\n" "$2" + printf "${cyan}Please use '--help' for assistance${norm}\n\n" + exit 1 + elif [ "$1" = "svc" ]; then + printf "${err}\nError: '%s %s': Service does not exist!${norm}\n\n" \ + "$2" "$3" + exit 1 + fi +} + +# cleanup +cleanup () { + # cleanup 503 if copied + if [ "$err503Copied" -eq 1 ]; then + if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then + printf "${warn}[%s] -- [WARNING] Could not remove 503 error page." \ + "$(stamp)" >> "$logFile" + printf " Web interface will not function until this file is " \ + >> "$logFile" + printf "removed --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) + else + printf "${cyan}[%s] -- [INFO] 503 error page removed --${norm}\n" \ + "$(stamp)" >> "$logFile" + fi + fi +} + +# call cleanup and then exit with error report +exitError () { + printf "${err}[%s] -- [ERROR] %s: %s --${norm}\n" \ + "$(stamp)" "$1" "$2" >> "$logFile" + cleanup + # note script completion with error + printf "${err}[%s] --- %s execution completed with error ---${norm}\n" \ + "$(stamp)" "$scriptName" >> "$logFile" + exit "$1" +} + +# display script help information +scriptHelp () { + printf "\n${cyan}%s\n" "--------------------------------------------------------------------------------" + + printf "Usage:${norm} %s [parameters]\n\n" "$scriptName" + printf "${cyan}Parameters ${yellow}(default value):${norm}\n" + printf "There are NO mandatory parameters. If a parameter is not " + printf "supplied, it's\n" + printf "default value will be used. In the case of a switch parameter, " + printf "it will\n" + printf "remain deactivated if not specified.\n\n" + + printf "${magenta}script related\n" + printf "${cyan}-c, --config, --details${norm}\n" + printf "Path to the configuration key/value pair file for this script.\n" + printf "${yellow}(scriptPath/scriptName.details)${norm}\n\n" + printf "${cyan}-h, -?, --help${norm}\n" + printf "This help screen.\n\n" + printf "${cyan}-l, --log${norm}\n" + printf "Path to write log file.\n" + printf "${yellow}(scriptPath/scriptName.log)${norm}\n\n" + printf "${cyan}-v, --verbose${norm}\n" + printf "Log borg output with increased verbosity (list all files). " + printf "Careful! Your\n" + printf "log file can get large very quickly! This is a switch value, " + printf "specifying it\n" + printf "will turn the option ON.\n" + printf "${yellow}(normal output, option is OFF)${norm}\n\n" + + printf "${magenta}503 functionality\n" + printf "${cyan}-5, --use-503${norm}\n" + printf "Copy an 'error 503' page to your webroot for your webserver to " + printf "find. This is a\n" + printf "switch value, specifying it will instruct the script to copy the " + printf "error page.\n" + printf "${yellow}(do NOT copy, option is OFF)${norm}\n\n" + printf "${cyan}--503-path${norm}\n" + printf "Path to the file you want copied to your webroot as the 'error " + printf "503' page during\n" + printf "backup operations.\n" + printf "${yellow}(scriptPath/503_backup.html)${norm}\n\n" + printf "${cyan}-w, --webroot${norm}\n" + printf "Path to where the 'error 503' file should be copied during " + printf "backups.\n" + printf "${yellow}(/usr/share/nginx/html)${norm}\n\n" + + printf "More details and examples of script usage can be found in the " + printf "repo wiki at\n" + printf "${yellow}https://git.asifbacchus.app/asif/pilerBackup/wiki${norm}\n" + + printf "${cyan}%s${norm}\n\n" "--------------------------------------------------------------------------------" +} + +# generate dynamic timestamps +stamp () { + (date +%F" "%T) +} + +# same as exitError but for signal captures +trapExit () { + printf "${err}[%s] -- [ERROR] 99: Caught signal --${norm}\n" \ + "$(stamp)" >> "$logFile" + cleanup + # note script completion with error + printf "${err}[%s] --- %s execution was terminated via signal ---${norm}\n" \ + "$(stamp)" "$scriptName" >> "$logFile" + exit 99 +} + +### end of functions + + +### default variable values + +## script related +# store logfile in the same directory as this script file using the same file +# name as the script but with the extension '.log' +scriptPath="$( CDPATH='' cd -- "$( dirname -- "$0" )" && pwd -P )" +scriptName="$( basename "$0" )" +logFile="$scriptPath/${scriptName%.*}.log" +warnCount=0 +configDetails="$scriptPath/${scriptName%.*}.details" +err503Copied=0 +includeXtra=0 +exclusions=0 +# borg output verbosity -- normal +borgCreateParams='--stats' +borgPruneParams='--list' + +# 503 related +use503=0 +err503Path="$scriptPath/503_backup.html" +err503File="${err503Path##*/}" +webroot="/usr/share/nginx/html" + + +### process startup parameters +while [ $# -gt 0 ]; do + case "$1" in + -h|-\?|--help) + # display help + scriptHelp + exit 0 + ;; + -l|--log) + # set log file location + if [ -n "$2" ]; then + logFile="${2%/}" + shift + else + badParam empty "$@" + fi + ;; + -v|--verbose) + # set verbose logging from borg + borgCreateParams='--list --stats' + borgPruneParams='--list' + ;; + -c|--config|--details) + # location of config details file + if [ -n "$2" ]; then + if [ -f "$2" ]; then + configDetails="${2%/}" + shift + else + badParam dne "$@" + fi + else + badParam empty "$@" + fi + ;; + -5|--use-503) + # enable copying 503 error page to webroot + use503=1 + ;; + --503-path) + # FULL path to 503 file + if [ -n "$2" ]; then + if [ -f "$2" ]; then + err503Path="${2%/}" + err503File="${2##*/}" + shift + else + badParam dne "$@" + fi + else + badParam empty "$@" + fi + ;; + -w|--webroot) + # path to webroot (copy 503) + if [ -n "$2" ]; then + if [ -d "$2" ]; then + webroot="${2%/}" + shift + else + badParam dne "$@" + fi + else + badParam empty "$@" + fi + ;; + *) + printf "${err}\nUnknown option: %s\n" "$1" + printf "${cyan}Use '--help' for valid options.{$norm}\n\n" + exit 1 + ;; + esac + shift +done + + +### check pre-requisites and default values +# does the details file exist? +if [ ! -f "$configDetails" ]; then + badParam dne "(--details default)" "$configDetails" +fi +# is borg installed? +if ! command -v borg > /dev/null; then + printf "\n${err}ERROR: BORG is not installed on this system!${norm}\n\n" + exit 3 +fi +# if 503 functionality is enabled, do 503 related files exist? +if [ "$use503" -eq 1 ]; then + if [ ! -f "$err503Path" ]; then + badParam dne "(--503-path default)" "$err503Path" + elif [ ! -d "$webroot" ]; then + badParam dne "(--webroot default)" "$webroot" + fi +fi + + +### start logging +printf "${magenta}[%s] --- Start %s execution ---${norm}\n" \ + "$(stamp)" "$scriptName" >> "$logFile" +printf "${cyan}[%s] -- [INFO] Log located at ${yellow}%s${cyan} --${norm}\n" \ + "$(stamp)" "$logFile" >> "$logFile" + + +### 503 functionality +if [ "$use503" -eq 1 ]; then + printf "${cyan}[%s] -- [INFO] Copying 503 error page to " \ + "$(stamp)" >> "$logFile" + printf "webroot -- ${norm}\n" >> "$logFile" + if ! \cp --force "${err503Path}" "${webroot}/${err503File}" 2>> "$logFile" + then + printf "${warn}[%s] -- [WARNING] Failed to copy 503 error page. " \ + "$(stamp)" >> "$logFile" + printf "Web users will NOT be notified --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) + else + printf "${ok}[%s] -- [SUCCESS] 503 error page copied --${norm}\n" \ + "$(stamp)" >> "$logFile" + # set cleanup flag + err503Copied=1 + fi +fi + + +### read details file to get variables needed to dump sql and run borg +# check if config details file was provided as a relative or absolute path +case "${configDetails}" in + /*) + # absolute path, no need to rewrite variable + . "${configDetails}" + ;; + *) + # relative path, prepend './' to create absolute path + . "./${configDetails}" + ;; +esac +printf "${cyan}[%s] -- [INFO] ${yellow}%s${cyan} imported --${norm}\n" \ + "$(stamp)" "$configDetails" >> "$logFile" + + +## create tmp directory +printf "${cyan}[%s] -- [INFO] Dumping SQL databases --${norm}\n" \ + "$(stamp)" >> "$logFile" +# create temporary directory to dump files before borg backup +if ! sqlDumpDir=$( mktemp -d 2>>"$logFile" ); then + exitError 111 "Could not create temporary directory to dump SQL files" +fi +# set cleanup flag +sqlCopied=1 +printf "${cyan}[%s] -- [INFO] SQL dump files will be temporarily stored in:" \ + "$(stamp)" >> "$logFile" +printf "\n${yellow}%s/${cyan} --${norm}\n" "$sqlDumpDir" >> "$logFile" +# create unique names for database dump files +sqlDump_ccnet="backup-$(date +%Y%m%d_%H%M%S)_${ccnetDB_name}.sql" +sqlDump_seafile="backup-$(date +%Y%m%d_%H%M%S)_${seafileDB_name}.sql" +sqlDump_seahub="backup-$(date +%Y%m%d_%H%M%S)_${seahubDB_name}.sql" + +## dump databases +# dump CCNET-DB +if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ + --opt "${ccnetDB_name}" > "${sqlDumpDir}/${sqlDump_ccnet}" \ + 2>> "$logFile"; then + exitError 115 "Could not dump ${ccnetDB_name} database" +fi +# dump SEAFILE-DB +if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ + --opt "${seafileDB_name}" > "${sqlDumpDir}/${sqlDump_seafile}" \ + 2>> "$logFile"; then + exitError 116 "Could not dump ${seafileDB_name} database" +fi +# dump CCNET-DB +if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ + --opt "${seahubDB_name}" > "${sqlDumpDir}/${sqlDump_seahub}" 2>> "$logFile"; then + exitError 117 "Could not dump ${seahubDB_name} database" +fi +printf "${ok}[%s] -- [SUCCESS] SQL databases dumped successfully --${norm}\n" \ + "$(stamp)" >> "$logFile" + + +### pre-backup tasks completed -- move to borg tasks +printf "${ok}[%s] -- [SUCCESS] Pre-backup tasks completed --${norm}\n" \ + "$(stamp)" >> "$logFile" + + +### Run borg variable checks +printf "${cyan}[%s] -- [INFO] Verifying supplied borg details --${norm}\n" \ + "$(stamp)" >> "$logFile" + +## verify borg base directory +if [ -z "${borgBaseDir}" ]; then + badDetails empty 'borgBaseDir' +elif [ ! -d "${borgBaseDir}" ]; then + badDetails dne 'borgBaseDir' +fi +printf "${magenta}details:borgBaseDir ${norm}-- ${ok}[OK]${norm}\n" \ + >> "$logFile" +export BORG_BASE_DIR="${borgBaseDir%/}" + +## check path to SSH keyfile +if [ -z "${borgSSHKey}" ]; then + badDetails empty 'borgSSHKey' +elif [ ! -f "${borgSSHKey}" ]; then + badDetails dne 'borgSSHKey' +fi +printf "${magenta}details:borgSSHKey ${norm}-- ${ok}[OK]${norm}\n" \ + >> "$logFile" +export BORG_RSH="ssh -i ${borgSSHKey}" + +## check borg repo connect string +if [ -z "${borgConnectRepo}" ]; then + badDetails empty 'borgConnectRepo' +fi +printf "${magenta}details:borgConnectRepo ${norm}-- ${ok}[OK]${norm}\n" \ + >> "$logFile" +export BORG_REPO="${borgConnectRepo}" + +## check borg repo password +if [ -n "${borgRepoPassphrase}" ]; then + printf "${magenta}details:borgRepoPassphrase ${norm}-- ${ok}[OK]${norm}\n" \ + >> "$logFile" + export BORG_PASSPHRASE="${borgRepoPassphrase}" +else + # if passwd is blank intentionally, this is insecure + printf "${warn} -- [WARNING] Using a borg repo without a password is an " \ + >> "$logFile" + printf "insecure configuration --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) + # if this was an accident, we need to provide a bogus passwd so borg fails + # otherwise it will sit forever just waiting for input + export BORG_PASSPHRASE="DummyPasswordSoBorgFails" +fi + +## export borg remote path, if specified +if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; fi + +## read additional files +if [ -n "${borgXtraListPath}" ]; then + # check if file actually exists + if [ ! -f "${borgXtraListPath}" ]; then + badDetails dne 'borgXtraListPath' + fi + # read file contents into concatenated list for echo to cmdline + while read -r xtraItem; do + if [ -z "${xtraList}" ]; then + xtraList="${xtraItem}" + else + xtraList="${xtraList} ${xtraItem}" + fi + done <> "$logFile" +includeXtra=1 +fi + +## check if exlusion list file is specified +if [ -n "${borgExcludeListPath}" ]; then + # check if the file actually exists + if [ ! -f "${borgExcludeListPath}" ]; then + badDetails dne 'borgExcludeListPath' + fi +exclusions=1 +fi + + +### create borg temp dir: +## python requires a writable temporary directory when unpacking borg and +## executing commands. This defaults to /tmp but many systems mount /tmp with +## the 'noexec' option for security. Thus, we will use/create a 'tmp' folder +## within the BORG_BASE_DIR and instruct python to use that instead of /tmp + +# check if BORG_BASE_DIR/tmp exists, if not, create it +if [ ! -d "${borgBaseDir}/tmp" ]; then + if ! mkdir "${borgBaseDir}/tmp"; then + exitError 132 "Unable to create borg ${borgBaseDir}/tmp directory" + else + printf "${cyan}[%s] -- [INFO] Created ${yellow}%s/tmp " \ + "$(stamp)" "${borgBaseDir}" >> "$logFile" + printf "${cyan}--${norm}\n" >> "$logFile" + fi +fi +export TMPDIR="${borgBaseDir}/tmp" + + +### execute borg depending on whether extra files and/or exclusions are defined + +## construct the proper borg commandline +# base command +if [ "$exclusions" -eq 0 ]; then + borgCMD="borg --show-rc create ${borgCreateParams} \ + ::$(date +%Y-%m-%d_%H%M%S) \ + ${seafDir} \ + ${seafData} \ + ${sqlDumpDir}" +elif [ "$exclusions" -eq 1 ]; then + borgCMD="borg --show-rc create ${borgCreateParams} \ + --exclude-from ${borgExcludeListPath} \ + ::$(date +%Y-%m-%d_%H%M%S) \ + ${seafDir} \ + ${seafData} \ + ${sqlDumpDir}" +fi + +# add extra locations if defined +if [ "$includeXtra" -eq 1 ]; then + borgCMD="${borgCMD} ${xtraList}" +fi + +# execute borg +printf "${cyan}[%s] -- [INFO] Executing borg backup operation --${norm}\n" \ + "$(stamp)" >> "$logFile" +${borgCMD} 2>> "$logFile" +borgResult="$?" + +## check borg exit status +if [ "$borgResult" -eq 0 ]; then + printf "${ok}[%s] -- [SUCCESS] Borg backup completed " \ + "$(stamp)" >> "$logFile" + printf "successfully --${norm}\n" >> "$logFile" +elif [ "$borgResult" -eq 1 ]; then + printf "${warn}[%s] -- [WARNING] Borg completed with warnings. " \ + "$(stamp)" >> "$logFile" + printf "Review this logfile for details --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) +elif [ "$borgResult" -ge 2 ]; then + err_1="Borg exited with a critical error. Please review this log file" + err_2="for details." + exitError 138 "$err_1 $err_2" +else + printf "${warn}[%s] -- [WARNING] Borg exited with unknown return code. " \ + "$(stamp)" >> "$logFile" + printf "Review this logfile for details --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) +fi + + +### execute borg prune if paramters are provided, otherwise skip with a warning +if [ -n "${borgPruneSettings}" ]; then + printf "${cyan}[%s] -- [INFO] Executing borg prune operation --${norm}\n" \ + "$(stamp)" >> "$logFile" + borg prune --show-rc -v ${borgPruneParams} ${borgPruneSettings} \ + 2>> "$logFile" + borgPruneResult="$?" +else + printf "${warn}[%s] -- [WARNING] No prune parameters provided. " \ + "$(stamp)" >> "$logFile" + printf "Your archive will continue growing with each backup --${norm}\n" \ + >> "$logFile" + warnCount=$((warnCount+1)) +fi + +## report on prune operation if executed +if [ -n "${borgPruneResult}" ]; then + if [ "${borgPruneResult}" -eq 0 ]; then + printf "${ok}[%s] -- [SUCCESS] Borg prune completed --${norm}\n" \ + "$(stamp)" >> "$logFile" + elif [ "$borgPruneResult" -eq 1 ]; then + printf "${warn}[%s] -- [WARNING] Borg prune completed with warnings. " \ + "$(stamp)" >> "$logFile" + printf "Review this logfile for details --${norm}\n" >> "$logFile" + warnCount=$((warnCount+1)) + elif [ "$borgPruneResult" -ge 2 ]; then + err_1="Borg prune exited with a critical error. Please review this" + err_2="log file for details." + exitError 139 "$err_1 $err_2" + else + printf "${warn}[%s] -- [WARNING] Borg prune exited with an unknown " \ + "$(stamp)" >> "$logFile" + printf "return code. Review this logfile for details --${norm}\n" \ + >> "$logFile" + warnCount=$((warnCount+1)) + fi +fi + + +### all processes successfully completed, cleanup and exit gracefully + +# note successful completion of borg commands +printf "${ok}[%s] -- [SUCCESS] Backup operations completed --${norm}\n" \ + "$(stamp)" >> "$logFile" + +# cleanup +cleanup + +# note complete success, tally warnings and exit +printf "${ok}[%s] -- [SUCCESS] All processes completed successfully --" \ + "${norm}\n" \ + "$(stamp)" >> "$logFile" +printf "${magenta}[%s] --- %s execution completed ---${norm}\n" \ + "$(stamp)" "$scriptName" >> "$logFile" +if [ "$warnCount" -gt 0 ]; then + printf "${warn}%s warnings issued!${norm}\n" "${warnCount}" >> "$logFile" +else + printf "${ok}0 warnings issued.${norm}\n" >> "$logFile" +fi +exit 0 + + + +### error codes +# 1: parameter error +# 2: not run as root +# 3: borg not installed +# 99: TERM signal trapped +# 111: could not create tmp dir for EML dump from piler-export +# 112: problem during piler-export operation +# 130: null configuration variable in details file +# 131: invalid configuration variable in details file +# 138: borg exited with a critical error +# 139: borg prune exited with a critical error \ No newline at end of file