Compare commits
31 Commits
04bc748816
...
f57841b69a
Author | SHA1 | Date | |
---|---|---|---|
|
f57841b69a | ||
|
4d407887d5 | ||
|
6f59804cfc | ||
|
b5247a085e | ||
|
2432139f3d | ||
|
5f0eddf22a | ||
|
09c1d903ba | ||
|
51928d4327 | ||
|
a7f506efb0 | ||
|
0993083a7b | ||
|
5448848c36 | ||
|
5667204378 | ||
|
ca1186d855 | ||
|
f8561a3aab | ||
|
1b68616e54 | ||
|
51120491d5 | ||
|
abdbb44922 | ||
|
ca62d81368 | ||
|
72e396fa64 | ||
|
992f8ef16e | ||
|
9d7b7e1a94 | ||
|
4fb6315643 | ||
|
d9c662bb45 | ||
|
e9884a347f | ||
|
8a5a87db12 | ||
|
0d32aaf40a | ||
|
bb952a3aba | ||
|
dccf5e17d0 | ||
|
35ce452dec | ||
|
604d3da07d | ||
|
a5bc4549e9 |
@ -18,11 +18,8 @@
|
||||
|
||||
<body>
|
||||
<h1>Bad timing!</h1>
|
||||
<p>Seems you're trying to access me during my daily backup window. Don't
|
||||
worry though, I should be up and running again very soon.</p>
|
||||
<p>My average backup window duration is around 10 minutes during which time
|
||||
I'm quite busy copying your super-important emails to my ultra-secret hiding
|
||||
place so they stay safe in case anything ever happens to me!</p>
|
||||
<p>Seems you're trying to access me during my daily backup window. Don't worry though, I should be up and running again very soon.</p>
|
||||
<p>My average backup window duration is pretty short and I'm quite busy during that time copying your super-important stuff to my secure hiding place so they stay safe in case anything ever happens to me!</p>
|
||||
<h3><em>I'm really sorry for the delay. Please try me again soon!</em></h3>
|
||||
</body>
|
||||
|
61
backup/backup.details
Normal file
61
backup/backup.details
Normal file
@ -0,0 +1,61 @@
|
||||
#######
|
||||
### 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 ./backup.details
|
||||
### chmod 600 ./backup.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 recommend 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:mailcow/"
|
||||
|
||||
# password to access repo
|
||||
# this was set when the repo was initialized and, while optional, is HIGHLY
|
||||
# recommended for security
|
||||
borgRepoPassphrase="p@ssW0rd"
|
||||
|
||||
# keyfile to access repo
|
||||
# FULL PATH where the associated keyfile for your repo is located -- relevant
|
||||
# only if your repo requires a keyfile (i.e. 'keyfile' vs 'repokey') and if you
|
||||
# are not using the default keyfile location
|
||||
borgKeyfileLocation="/var/borgbackup/.config/borg/keys/server_address__repo_name"
|
||||
|
||||
# REQUIRED: path to text file containing a list (one per line) of files/
|
||||
# directories to include in your backup. Since this is a generic backup script,
|
||||
# nothing is defined by default. Therefore, ONLY files specified in this file
|
||||
# will be backed up!
|
||||
# see repo wiki for more details
|
||||
borgXtraListPath="/scripts/backup/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="/scripts/backup/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=14d --keep-daily=30 --keep-weekly=12 --keep-monthly=12"
|
||||
|
||||
# location of borg instance on your remote backup server
|
||||
# this is very often just "borg1"
|
||||
borgRemote="borg1"
|
795
backup/backup.sh
Executable file
795
backup/backup.sh
Executable file
@ -0,0 +1,795 @@
|
||||
#!/bin/sh
|
||||
|
||||
#######
|
||||
### mailcow backup using borgbackup
|
||||
### this assumes three things:
|
||||
### 1. standard mailcow-dockerized setup as per the docs
|
||||
### 2. using borg to perform backups to ssh-capable remote server
|
||||
### 3. remote repo already set-up and configured
|
||||
#######
|
||||
|
||||
|
||||
### text formatting presents
|
||||
if command -v tput > /dev/null; then
|
||||
bold=$(tput bold)
|
||||
cyan=$(tput setaf 6)
|
||||
err=$(tput bold)$(tput setaf 1)
|
||||
magenta=$(tput setaf 5)
|
||||
norm=$(tput sgr0)
|
||||
ok=$(tput setaf 2)
|
||||
warn=$(tput bold)$(tput setaf 3)
|
||||
width=$(tput cols)
|
||||
yellow=$(tput setaf 3)
|
||||
else
|
||||
bold=""
|
||||
cyan=""
|
||||
err=""
|
||||
magenta=""
|
||||
norm=""
|
||||
ok=""
|
||||
warn=""
|
||||
width=80
|
||||
yellow=""
|
||||
fi
|
||||
|
||||
|
||||
### 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 "\n%sError: '%s %s'\n" "$err" "$2" "$3"
|
||||
printf "file or directory does not exist.%s\n\n" "$norm"
|
||||
exit 1
|
||||
elif [ "$1" = "empty" ]; then
|
||||
printf "\n%sError: '%s' cannot have a NULL (empty) value.\n" "$err" "$2"
|
||||
printf "%sPlease use '--help' for assistance%s\n\n" "$cyan" "$norm"
|
||||
exit 1
|
||||
elif [ "$1" = "svc" ]; then
|
||||
printf "\n%sError: '%s %s': Service does not exist!%s\n\n" \
|
||||
"$err" "$2" "$3" "$norm"
|
||||
exit 1
|
||||
elif [ "$1" = "user" ]; then
|
||||
printf "\n%sError: '%s %s': User does not exist!%s\n\n" \
|
||||
"$err" "$2" "$3" "$norm"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# cleanup
|
||||
cleanup() {
|
||||
# cleanup 503 if copied
|
||||
if [ "$err503Copied" -eq 1 ]; then
|
||||
if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then
|
||||
printf "%s[%s] -- [WARNING] Could not remove 503 error page." \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf " Web interface will not function until this file is " \
|
||||
>> "$logFile"
|
||||
printf "removed --%s\n" "$norm" >> "$logFile"
|
||||
warnCount=$((warnCount+1))
|
||||
else
|
||||
printf "%s[%s] -- [INFO] 503 error page removed --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
fi
|
||||
fi
|
||||
# cleanup SQL dump directory if created
|
||||
if [ "$sqlDumpDirCreated" -eq 1 ]; then
|
||||
if ! rm -rf "$sqlDumpDir" 2>>"$logFile"; then
|
||||
printf "%s[%s] -- [WARNING] Could not remove temporary SQL-dump directory. Sorry for the mess. --%s\n" \
|
||||
"$warn" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
printf "%s[%s] -- [INFO] Temporary SQL-dump directory removed successfully --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
fi
|
||||
fi
|
||||
# start docker containers (no harm if they are already running)
|
||||
doDocker start postfix
|
||||
if [ "$dockerResultState" = "true" ]; then
|
||||
printf "%s[%s] -- [INFO] POSTFIX container is running --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 102 'Could not start POSTFIX container.'
|
||||
fi
|
||||
doDocker start dovecot
|
||||
if [ "$dockerResultState" = "true" ]; then
|
||||
printf "%s[%s] -- [INFO] DOVECOT container is running --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 102 'Could not start DOVECOT container.'
|
||||
fi
|
||||
}
|
||||
|
||||
doDocker() {
|
||||
containerName="$( docker ps -a --format '{{ .Names }}' --filter name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1 )"
|
||||
|
||||
# determine action to take
|
||||
if [ "$1" = "stop" ]; then
|
||||
printf "%s[%s] -- [INFO] Stopping %s-mailcow container --%s\n" \
|
||||
"$cyan" "$(stamp)" "$2" "$norm" >> "$logFile"
|
||||
docker-compose -f "$mcDockerCompose" stop --timeout "$dockerStopTimeout" "$2-mailcow" 2>> "$logFile"
|
||||
# set result vars
|
||||
dockerResultState="$( docker inspect -f '{{ .State.Running }}' $containerName )"
|
||||
dockerResultExit="$( docker inspect -f '{{ .State.ExitCode }}' $containerName )"
|
||||
elif [ "$1" = "start" ]; then
|
||||
printf "%s[%s] -- [INFO] Starting %s-mailcow container --%s\n" \
|
||||
"$cyan" "$(stamp)" "$2" "$norm" >> "$logFile"
|
||||
docker-compose -f "$mcDockerCompose" start "$2-mailcow" 2>> "$logFile"
|
||||
# set result vars
|
||||
dockerResultState="$( docker inspect -f '{{ .State.Running }}' $containerName )"
|
||||
fi
|
||||
}
|
||||
|
||||
# call cleanup and then exit with error report
|
||||
exitError() {
|
||||
printf "%s[%s] -- [ERROR] %s: %s --%s\n" \
|
||||
"$err" "$(stamp)" "$1" "$2" "$norm" >> "$logFile"
|
||||
cleanup
|
||||
# note script completion with error
|
||||
printf "%s[%s] --- %s execution completed with error ---%s\n" \
|
||||
"$err" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
|
||||
exit "$1"
|
||||
}
|
||||
|
||||
# display script help information
|
||||
scriptHelp() {
|
||||
newline
|
||||
printf "%sUsage: %s [parameters]%s\n\n" "$bold" "$scriptName" "$norm"
|
||||
textblock "There are NO mandatory parameters. If a parameter is not supplied, its default value will be used. In the case of a switch parameter, it will remain DEactivated if NOT specified."
|
||||
newline
|
||||
textblock "Parameters are listed then followed by a description of their effect on the following line. Finally, if a default value exists, it will be listed on the next line in (parentheses)."
|
||||
newline
|
||||
textblock "${magenta}--- script related parameters ---${norm}"
|
||||
newline
|
||||
switchTextblock "-c | --config | --details"
|
||||
textblock "Path to the configuration key/value-pair file for this script."
|
||||
defaultsTextblock "(scriptPath/scriptName.details)"
|
||||
newline
|
||||
switchTextblock "-h | -? | --help"
|
||||
textblock "This help screen"
|
||||
newline
|
||||
switchTextblock "-l | --log"
|
||||
textblock "Path to write log file"
|
||||
defaultsTextblock "(scriptPath/scriptName.log)"
|
||||
newline
|
||||
switchTextblock "[SWITCH] -v | --verbose"
|
||||
textblock "Log borg output with increased verbosity (list all files). Careful! Your log file can get very large very quickly!"
|
||||
defaultsTextblock "(normal output, option is OFF)"
|
||||
newline
|
||||
textblock "${magenta}--- 503 functionality ---${norm}"
|
||||
newline
|
||||
switchTextblock "[SWITCH] -5 | --use-503"
|
||||
textblock "Copy an 'error 503' page/indicator file to your webroot for your webserver to find. Specifying this option will enable other 503 options."
|
||||
defaultsTextblock "(do NOT copy, option is OFF)"
|
||||
newline
|
||||
switchTextblock "--503-path"
|
||||
textblock "Path to the file you want copied to your webroot as the 'error 503' page."
|
||||
defaultsTextblock "(scriptPath/503_backup.html)"
|
||||
newline
|
||||
switchTextblock "-w | --webroot"
|
||||
textblock "Path to where the 'error 503' file should be copied."
|
||||
defaultsTextblock "(/usr/share/nginx/html/)"
|
||||
newline
|
||||
textblock "${magenta}--- mailcow related ---${norm}"
|
||||
newline
|
||||
switchTextblock "-d | --docker-compose"
|
||||
textblock "Path to mailcow's 'docker-compose.yml' file."
|
||||
defaultsTextblock "(/opt/mailcow-dockerized/docker-compose.yml)"
|
||||
newline
|
||||
switchTextblock "-m | --mailcow-config"
|
||||
textblock "Path to mailcow configuration file ('mailcow.conf')."
|
||||
defaultsTextblock "(/opt/mailcow-dockerized/mailcow.conf)"
|
||||
newline
|
||||
switchTextblock "-t1 | --dockerStartTimeout"
|
||||
textblock "Seconds to wait for docker containers to start."
|
||||
defaultsTextblock "(180)"
|
||||
newline
|
||||
switchTextblock "-t2 | --dockerStopTimeout"
|
||||
textblock "Seconds to wait for docker containers to stop."
|
||||
defaultsTextblock "(120)"
|
||||
newline
|
||||
textblock "More details and examples of script usage can be found in the repo wiki at ${yellow}https://git.asifbacchus.app/asif/MailcowBackup/wiki${norm}"
|
||||
newline
|
||||
}
|
||||
|
||||
# generate dynamic timestamps
|
||||
stamp() {
|
||||
(date +%F" "%T)
|
||||
}
|
||||
|
||||
textblock() {
|
||||
printf "%s\n" "$1" | fold -w "$width" -s
|
||||
}
|
||||
|
||||
defaultsTextblock() {
|
||||
printf "%s%s%s\n" "$yellow" "$1" "$norm"
|
||||
}
|
||||
|
||||
switchTextblock() {
|
||||
printf "%s%s%s\n" "$cyan" "$1" "$norm"
|
||||
}
|
||||
|
||||
# print a blank line
|
||||
newline() {
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# same as exitError but for signal captures
|
||||
trapExit() {
|
||||
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" \
|
||||
"$err" "$(stamp)" "$norm" >> "$logFile"
|
||||
cleanup
|
||||
# note script completion with error
|
||||
printf "%s[%s] --- %s execution was terminated via signal ---%s\n" \
|
||||
"$err" "$(stamp)" "$scriptName" "$norm" >> "$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
|
||||
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"
|
||||
|
||||
# mailcow/docker related
|
||||
mcConfig='/opt/mailcow-dockerized/mailcow.conf'
|
||||
mcDockerCompose="/opt/mailcow-dockerized/docker-compose.yml"
|
||||
dockerStartTimeout=180
|
||||
dockerStopTimeout=120
|
||||
|
||||
|
||||
### 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
|
||||
;;
|
||||
-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
|
||||
;;
|
||||
-v|--verbose)
|
||||
# set verbose logging from borg
|
||||
borgCreateParams='--list --stats'
|
||||
borgPruneParams='--list'
|
||||
;;
|
||||
-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
|
||||
;;
|
||||
-d|--docker-compose)
|
||||
# FULL path to mailcow docker-compose file
|
||||
if [ -n "$2" ]; then
|
||||
if [ -f "$2" ]; then
|
||||
mcDockerCompose="$2"
|
||||
shift
|
||||
else
|
||||
badParam dne "$@"
|
||||
fi
|
||||
else
|
||||
badParam empty "$@"
|
||||
fi
|
||||
;;
|
||||
-m|--mailcow-config)
|
||||
# FULL path to mailcow configuration file
|
||||
if [ -n "$2" ]; then
|
||||
if [ -f "$2" ]; then
|
||||
mcConfig="$2"
|
||||
shift
|
||||
else
|
||||
badParam dne "$@"
|
||||
fi
|
||||
else
|
||||
badParam empty "$@"
|
||||
fi
|
||||
;;
|
||||
-t1|--timeout-start)
|
||||
if [ -z "$2" ]; then
|
||||
badParam empty "$@"
|
||||
else
|
||||
dockerStartTimeout="$2"
|
||||
fi
|
||||
;;
|
||||
-t2|--timeout-stop)
|
||||
if [ -z "$2" ]; then
|
||||
badParam empty "$@"
|
||||
else
|
||||
dockerStopTimeout="$2"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
printf "\n%sUnknown option: %s\n" "$err" "$1"
|
||||
printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
|
||||
### check pre-requisites and default values
|
||||
# check if running as root, otherwise exit
|
||||
if [ "$( id -u )" -ne 0 ]; then
|
||||
printf "\n%sERROR: script MUST be run as ROOT%s\n\n" "$err" "$norm"
|
||||
exit 2
|
||||
fi
|
||||
# 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%sERROR: BORG is not installed on this system!%s\n\n" "$err" "$norm"
|
||||
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
|
||||
# verify mailcow.conf location and extract path
|
||||
if [ ! -f "$mcConfig" ]; then
|
||||
badParam dne "(--mailcow-config)" "$mcConfig"
|
||||
fi
|
||||
# verify docker-compose file exists
|
||||
if [ ! -f "$mcDockerCompose" ]; then
|
||||
badParam dne "(--docker-compose)" "$mcDockerCompose"
|
||||
fi
|
||||
|
||||
|
||||
### read mailcow.conf and set vars as needed
|
||||
. "$mcConfig"
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout"
|
||||
|
||||
|
||||
### start logging
|
||||
printf "%s[%s] --- Start %s execution ---%s\n" \
|
||||
"$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
|
||||
printf "%s[%s] -- [INFO] Log located at %s%s%s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$yellow" "$logFile" "$cyan" "$norm" >> "$logFile"
|
||||
|
||||
|
||||
### get location of docker volumes
|
||||
dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1)
|
||||
printf "%s[%s] -- [INFO] Using MAIL volume: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$dockerVolumeMail" "$norm" >> "$logFile"
|
||||
dockerVolumeRspamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_rspamd-vol-1)
|
||||
printf "%s[%s] -- [INFO] Using RSPAMD volume: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$dockerVolumeRspamd" "$norm" >> "$logFile"
|
||||
dockerVolumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_postfix-vol-1)
|
||||
printf "%s[%s] -- [INFO] Using POSTFIX volume: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$dockerVolumePostfix" "$norm" >> "$logFile"
|
||||
dockerVolumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_redis-vol-1)
|
||||
printf "%s[%s] -- [INFO] Using REDIS volume: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$dockerVolumeRedis" "$norm" >> "$logFile"
|
||||
dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1)
|
||||
printf "%s[%s] -- [INFO] Using MAILCRYPT volume: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$dockerVolumeCrypt" "$norm" >> "$logFile"
|
||||
|
||||
|
||||
### read details file to get variables needed 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 "%s[%s] -- [INFO] %s%s%s imported --%s\n" \
|
||||
"$cyan" "$(stamp)" "$yellow" "$configDetails" "$cyan" "$norm" >> "$logFile"
|
||||
|
||||
|
||||
### Run borg variable checks
|
||||
printf "%s[%s] -- [INFO] Verifying supplied borg details --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
|
||||
## read additional files -- this is required otherwise nothing to backup!
|
||||
if [ -z "${borgXtraListPath}" ]; then
|
||||
badDetails empty 'xtraLocations'
|
||||
else
|
||||
# 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 <<EOF
|
||||
$( sed -e '/^\s*#.*$/d' -e '/^\s*$/d' "${borgXtraListPath}" )
|
||||
EOF
|
||||
printf "%sdetails:borgXtraListPath %s-- %s[OK]%s\n" \
|
||||
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
fi
|
||||
|
||||
## verify borg base directory
|
||||
if [ -z "${borgBaseDir}" ]; then
|
||||
badDetails empty 'borgBaseDir'
|
||||
elif [ ! -d "${borgBaseDir}" ]; then
|
||||
badDetails dne 'borgBaseDir'
|
||||
fi
|
||||
printf "%sdetails:borgBaseDir %s-- %s[OK]%s\n" \
|
||||
"$magenta" "$norm" "$ok" "$norm" >> "$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 "%sdetails:borgSSHKey %s-- %s[OK]%s\n" \
|
||||
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
export BORG_RSH="ssh -i ${borgSSHKey}"
|
||||
|
||||
## check borg repo connect string
|
||||
if [ -z "${borgConnectRepo}" ]; then
|
||||
badDetails empty 'borgConnectRepo'
|
||||
fi
|
||||
printf "%sdetails:borgConnectRepo %s-- %s[OK]%s\n" \
|
||||
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
export BORG_REPO="${borgConnectRepo}"
|
||||
|
||||
## check borg repo password
|
||||
if [ -n "${borgRepoPassphrase}" ]; then
|
||||
printf "%sdetails:borgRepoPassphrase %s-- %s[OK]%s\n" \
|
||||
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
export BORG_PASSPHRASE="${borgRepoPassphrase}"
|
||||
else
|
||||
# if passwd is blank intentionally, this is insecure
|
||||
printf "%s-- [WARNING] Using a borg repo without a password is an " \
|
||||
"$warn" >> "$logFile"
|
||||
printf "insecure configuration --%s\n" "$norm">> "$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
|
||||
|
||||
## check borg repository keyfile location
|
||||
if [ -z "${borgKeyfileLocation}" ]; then
|
||||
printf "%sdetails:borgKeyfileLocation %s-- %s[DEFAULT]%s\n" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
else
|
||||
# check if keyfile location exists
|
||||
if [ ! -f "${borgKeyfileLocation}" ]; then
|
||||
badDetails dne 'borgKeyfileLocation'
|
||||
fi
|
||||
printf "%sdetails:borgKeyfileLocation %s-- %s[OK]%s\n" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
|
||||
export BORG_KEY_FILE="${borgKeyfileLocation}"
|
||||
fi
|
||||
|
||||
## export borg remote path, if specified
|
||||
if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; 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
|
||||
|
||||
|
||||
### set location of sql dump
|
||||
# this is done before resetting default TMP dir for borg
|
||||
if ! sqlDumpDir=$( mktemp -d 2>/dev/null ); then
|
||||
exitError 115 'Unable to create temp directory for SQL dump.'
|
||||
else
|
||||
sqlDumpFile="backup-$( date +%Y%m%d_%H%M%S ).sql"
|
||||
sqlDumpDirCreated=1
|
||||
printf "%s[%s] -- [INFO] SQL dump file will be stored at: %s --%s\n" \
|
||||
"$cyan" "$(stamp)" "$sqlDumpDir/$sqlDumpFile" "$norm" >> "$logFile"
|
||||
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 "%s[%s] -- [INFO] Created %s%s/tmp " \
|
||||
"$cyan" "$(stamp)" "$yellow" "${borgBaseDir}" >> "$logFile"
|
||||
printf "%s--%s\n" "$cyan" "$norm">> "$logFile"
|
||||
fi
|
||||
fi
|
||||
export TMPDIR="${borgBaseDir}/tmp"
|
||||
|
||||
|
||||
### 503 functionality
|
||||
if [ "$use503" -eq 1 ]; then
|
||||
printf "%s[%s] -- [INFO] Copying 503 error page to " \
|
||||
"$cyan" "$(stamp)" >> "$logFile"
|
||||
printf "webroot -- %s\n" "$norm">> "$logFile"
|
||||
if ! cp --force "${err503Path}" "${webroot}/${err503File}" 2>> "$logFile"
|
||||
then
|
||||
printf "%s[%s] -- [WARNING] Failed to copy 503 error page. " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "Web users will NOT be notified --%s\n" "$norm" >> "$logFile"
|
||||
warnCount=$((warnCount+1))
|
||||
else
|
||||
printf "%s[%s] -- [SUCCESS] 503 error page copied --%s\n" \
|
||||
"$ok" "$(stamp)" "$norm" >> "$logFile"
|
||||
# set cleanup flag
|
||||
err503Copied=1
|
||||
fi
|
||||
fi
|
||||
|
||||
### change to mailcow directory so docker commands execute properly
|
||||
cd "${mcConfig%/*}" || exitError 100 'Could not change to mailcow directory.'
|
||||
|
||||
### stop postfix and dovecot mail containers to prevent mailflow during backup
|
||||
doDocker stop postfix
|
||||
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
|
||||
printf "%s[%s] -- [INFO] POSTFIX container stopped --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 101 'Could not stop POSTFIX container.'
|
||||
fi
|
||||
doDocker stop dovecot
|
||||
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
|
||||
printf "%s[%s] -- [INFO] DOVECOT container stopped --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 101 'Could not stop DOVECOT container.'
|
||||
fi
|
||||
|
||||
|
||||
### dump SQL
|
||||
printf "%s[%s] -- [INFO] Dumping mailcow SQL database --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
docker-compose exec -T mysql-mailcow mysqldump --default-character-set=utf8mb4 \
|
||||
-u${DBUSER} -p${DBPASS} ${DBNAME} > "$sqlDumpDir/$sqlDumpFile" 2>> "$logFile"
|
||||
dumpResult=$( docker-compose exec -T mysql-mailcow echo "$?" )
|
||||
if [ "$dumpResult" -eq 0 ]; then
|
||||
printf "%s[%s] -- [INFO] SQL database dumped successfully --%s\n" \
|
||||
"cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 118 'There was an error dumping the mailcow SQL database.'
|
||||
fi
|
||||
|
||||
|
||||
### dump redis inside container
|
||||
# delete old redis dump if it exists
|
||||
if [ -f "$dockerVolumeRedis/dump.rdb" ]; then
|
||||
rm -f "$dockerVolumeRedis/dump.rdb"
|
||||
fi
|
||||
# dump redis
|
||||
printf "%s[%s] -- [INFO] Dumping mailcow redis database --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
docker-compose exec -T redis-mailcow redis-cli save >> "$logFile" 2>&1
|
||||
rdumpResult=$( docker-compose exec -T redis-mailcow echo "$?" )
|
||||
if [ "$rdumpResult" -eq 0 ]; then
|
||||
printf "%s[%s] -- [INFO] mailcow redis dumped successfully --%s\n" \
|
||||
"cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
else
|
||||
exitError 119 'There was an error dumping the mailcow redis database.'
|
||||
fi
|
||||
|
||||
|
||||
### execute borg depending on whether exclusions are defined
|
||||
printf "%s[%s] -- [INFO] Pre-backup tasks completed, calling borgbackup --%s\n" "$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
|
||||
## 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) \
|
||||
${sqlDumpDir} \
|
||||
${dockerVolumeMail} \
|
||||
${dockerVolumeRspamd} \
|
||||
${dockerVolumePostfix} \
|
||||
${dockerVolumeRedis} \
|
||||
${dockerVolumeCrypt} \
|
||||
${xtraList}"
|
||||
elif [ "$exclusions" -eq 1 ]; then
|
||||
borgCMD="borg --show-rc create ${borgCreateParams} \
|
||||
--exclude-from ${borgExcludeListPath} \
|
||||
::$(date +%Y-%m-%d_%H%M%S) \
|
||||
${sqlDumpDir} \
|
||||
${dockerVolumeMail} \
|
||||
${dockerVolumeRspamd} \
|
||||
${dockerVolumePostfix} \
|
||||
${dockerVolumeRedis} \
|
||||
${dockerVolumeCrypt} \
|
||||
${xtraList}"
|
||||
fi
|
||||
|
||||
# execute borg
|
||||
printf "%s[%s] -- [INFO] Executing borg backup operation --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
${borgCMD} 2>> "$logFile"
|
||||
borgResult="$?"
|
||||
|
||||
## check borg exit status
|
||||
if [ "$borgResult" -eq 0 ]; then
|
||||
printf "%s[%s] -- [SUCCESS] Borg backup completed --%s\n" \
|
||||
"$ok" "$(stamp)" "$norm" >> "$logFile"
|
||||
elif [ "$borgResult" -eq 1 ]; then
|
||||
printf "%s[%s] -- [WARNING] Borg completed with warnings. " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "Review this logfile for details --%s\n" "$norm">> "$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 "%s[%s] -- [WARNING] Borg exited with unknown return code. " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "Review this logfile for details --%s\n" "$norm">> "$logFile"
|
||||
warnCount=$((warnCount+1))
|
||||
fi
|
||||
|
||||
|
||||
### execute borg prune if paramters are provided, otherwise skip with a warning
|
||||
if [ -n "${borgPruneSettings}" ]; then
|
||||
printf "%s[%s] -- [INFO] Executing borg prune operation --%s\n" \
|
||||
"$cyan" "$(stamp)" "$norm" >> "$logFile"
|
||||
borg prune --show-rc -v ${borgPruneParams} ${borgPruneSettings} \
|
||||
2>> "$logFile"
|
||||
borgPruneResult="$?"
|
||||
else
|
||||
printf "%s[%s] -- [WARNING] No prune parameters provided. " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "Your archive will continue growing with each backup --%s\n" \
|
||||
"$norm" >> "$logFile"
|
||||
warnCount=$((warnCount+1))
|
||||
fi
|
||||
|
||||
## report on prune operation if executed
|
||||
if [ -n "${borgPruneResult}" ]; then
|
||||
if [ "${borgPruneResult}" -eq 0 ]; then
|
||||
printf "%s[%s] -- [SUCCESS] Borg prune completed --%s\n" \
|
||||
"$ok" "$(stamp)" "$norm" >> "$logFile"
|
||||
elif [ "$borgPruneResult" -eq 1 ]; then
|
||||
printf "%s[%s] -- [WARNING] Borg prune completed with warnings. " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "Review this logfile for details --%s\n" "$norm" >> "$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 "%s[%s] -- [WARNING] Borg prune exited with an unknown " \
|
||||
"$warn" "$(stamp)" >> "$logFile"
|
||||
printf "return code. Review this logfile for details --%s\n" \
|
||||
"$norm" >> "$logFile"
|
||||
warnCount=$((warnCount+1))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
### all processes successfully completed, cleanup and exit gracefully
|
||||
|
||||
# note successful completion of borg commands
|
||||
printf "%s[%s] -- [SUCCESS] Backup operations completed --%s\n" \
|
||||
"$ok" "$(stamp)" "$norm" >> "$logFile"
|
||||
|
||||
# cleanup
|
||||
cleanup
|
||||
|
||||
# note complete success, tally warnings and exit
|
||||
printf "%s[%s] -- [SUCCESS] All processes completed --%s\n" \
|
||||
"$ok" "$(stamp)" "$norm" >> "$logFile"
|
||||
printf "%s[%s] --- %s execution completed ---%s\n" \
|
||||
"$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
|
||||
if [ "$warnCount" -gt 0 ]; then
|
||||
printf "%s%s warnings issued!%s\n" "$warn" "${warnCount}" "$norm" >> "$logFile"
|
||||
else
|
||||
printf "%s0 warnings issued.%s\n" "$ok" "$norm" >> "$logFile"
|
||||
fi
|
||||
exit 0
|
||||
|
||||
|
||||
### error codes
|
||||
# 1: parameter error
|
||||
# 2: not run as root
|
||||
# 3: borg not installed
|
||||
# 99: TERM signal trapped
|
||||
# 100: could not change to mailcow-dockerized directory
|
||||
# 101: could not stop container(s)
|
||||
# 102: could not start container(s)
|
||||
# 115: unable to create temp dir for SQL dump
|
||||
# 118: error dumping SQL database
|
||||
# 119: error dumping redis database
|
||||
# 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
|
2
backup/excludeLocations.borg
Normal file
2
backup/excludeLocations.borg
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
EOF
|
60
backup/xtraLocations.borg
Normal file
60
backup/xtraLocations.borg
Normal file
@ -0,0 +1,60 @@
|
||||
# Files and directories listed here will be included in your borg backup
|
||||
#
|
||||
# Good candidates for inclusion would be things like your mailcow configuration
|
||||
# files, customized docker-compose overrides, your SSL certificates, etc.
|
||||
#
|
||||
# List the path to files/directories one per line.
|
||||
# Any blank lines will be ignored.
|
||||
# Any lines starting with '#' will be ignored as a comment.
|
||||
# For consistency, you should include the trailing slash for directories.
|
||||
|
||||
# these examples are for a very basic Debian machine hosting mailcow
|
||||
|
||||
|
||||
### important system configuration files
|
||||
|
||||
# basic configuration
|
||||
/etc/fstab
|
||||
/etc/network/interfaces
|
||||
/etc/network/interfaces.d/
|
||||
/etc/systemd/timesyncd.conf
|
||||
|
||||
# ssh configuration and host keys
|
||||
/etc/ssh/
|
||||
|
||||
# apt configuration
|
||||
/etc/apt/sources.list
|
||||
/etc/apt/sources.list.d/
|
||||
/etc/apt/listchanges.conf
|
||||
/etc/apt/apt.conf.d/50unattended-upgrades
|
||||
/etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
# user profile defaults and configurations
|
||||
/etc/profile
|
||||
/etc/bash.bashrc
|
||||
/etc/skel/
|
||||
/etc/nanorc
|
||||
|
||||
# selected root user files
|
||||
/root/.bashrc
|
||||
/root/.ssh/
|
||||
|
||||
# scripts
|
||||
/scripts/
|
||||
|
||||
|
||||
### important programs and configurations
|
||||
|
||||
# name of program for reference
|
||||
# include the paths to important configuration files/directories and/or
|
||||
# data directories
|
||||
|
||||
# mailcow configuration (example)
|
||||
/opt/mailcow-dockerized/
|
||||
|
||||
# NGINX (example)
|
||||
/etc/nginx/
|
||||
/usr/share/nginx/html/
|
||||
|
||||
# LetsEncrypt (example)
|
||||
/etc/letsencrypt/
|
@ -1,881 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
### Text formatting presets
|
||||
normal="\e[0m"
|
||||
bold="\e[1m"
|
||||
default="\e[39m"
|
||||
err="\e[1;31m"
|
||||
warn="\e[1;93m"
|
||||
ok="\e[32m"
|
||||
lit="\e[93m"
|
||||
op="\e[39m"
|
||||
info="\e[96m"
|
||||
note="\e[95m"
|
||||
|
||||
|
||||
### Functions ###
|
||||
|
||||
### scriptHelp -- display usage information for this script
|
||||
function scriptHelp {
|
||||
echo -e "${bold}${note}\n${scriptName} usage instructions:\n${normal}"
|
||||
echo -e "${default}This script performs a backup of your Mailcow system"
|
||||
echo -e "assuming a fairly standard set up such as outlined at"
|
||||
echo -e "${lit}https://mytechiethoughts.com${default}. Full details about"
|
||||
echo -e "this script can be found at that site."
|
||||
echo -e "${bold}\nThe script performs the following tasks:${normal}${default}"
|
||||
echo -e "1. Stops mailflow (postfix & dovecot) containers."
|
||||
echo -e "2. Optionally copies a 503 error page to your webroot."
|
||||
echo -e "3. Dumps Mailcow's SQL DB to a temporary directory."
|
||||
echo -e "4. Invokes borgbackup to backup your SQL info, Mailcow settings"
|
||||
echo -e "\tand (raw) data files along with any other files you specify."
|
||||
echo -e "5. Removes temp files, the 503 error page and restores"
|
||||
echo -e "\tMailcow to operational status."
|
||||
echo -e "\nThe readme file included in this script's git contains detailed"
|
||||
echo -e "usage information. The following is a brief summary:\n"
|
||||
echo -e "${bold}***You MUST provide the full path to your mailcow"
|
||||
echo -e "configuration file using the '-m' parameter***${normal}${default}"
|
||||
echo -e "${bold}${note}\nMandatory parameters:${normal}${default}"
|
||||
echo -e "${lit}\n-m, File name of the Mailcow build configuration file${default}"
|
||||
echo -e "FULL PATH to your Mailcow master build configuration file containing"
|
||||
echo -e "all variables and configuration info unique to your Mailcow setup."
|
||||
echo -e "The path specified here is also used for all docker-related"
|
||||
echo -e "operations in this script."
|
||||
echo -e "${info}Default: <none>${default}"
|
||||
echo -e "${bold}${note}\nOptional parameters:${normal}${default}"
|
||||
echo -e "${lit}\n-1, Timeout for containers to STOP before error${default}"
|
||||
echo -e "The number of seconds to wait for a docker container to STOP"
|
||||
echo -e "before aborting the procedure and exiting this script with an"
|
||||
echo -e "error."
|
||||
echo -e "${info}Default: 120 seconds${default}"
|
||||
echo -e "${lit}\n-2, Timeout for containers to START before error${default}"
|
||||
echo -e "The number of seconds to wait for a docker container to START"
|
||||
echo -e "before aborting the procedure and exiting this script with an"
|
||||
echo -e "error."
|
||||
echo -e "${info}Default: 180 seconds${default}"
|
||||
echo -e "${lit}\n-5, Location of 503 error page file${default}"
|
||||
echo -e "FULL PATH to the 503 error page HTML file you want copied to your"
|
||||
echo -e "webroot to inform users the server is down during the backup. If"
|
||||
echo -e "you don't specify a path/file, the default will be used. If the"
|
||||
echo -e "default cannot be found, a warning will be logged and the script"
|
||||
echo -e "will continue."
|
||||
echo -e "${info}Default: ScriptPath/503.html${default}"
|
||||
echo -e "${lit}\n-b, Location of file with borg repo details${default}"
|
||||
echo -e "FULL PATH to the plain text file containing all information needed"
|
||||
echo -e "to connect and process your borg repo. Details on the structure of"
|
||||
echo -e "this file are in the readme and on ${lit}https://mytechiethoughts.com${default}"
|
||||
echo -e "${info}Default: ScriptPath/nc_borg.details${default}"
|
||||
echo -e "${lit}\n-d, File name of the docker-compose configuration file${default}"
|
||||
echo -e "Name of the docker-compose configuration file that Mailcow uses"
|
||||
echo -e "to build/start/stop all containers. This will only be searched"
|
||||
echo -e "for in the path provided for your mailcow configuration file."
|
||||
echo -e "${info}Default: docker-compose.yml${default}"
|
||||
echo -e "${lit}\n-l, Location to save log file${default}"
|
||||
echo -e "This script writes a detailed log file of all activities. It is"
|
||||
echo -e "structured in an way easy for log parsers (like Logwatch) to read."
|
||||
echo -e "${info}Default: ScriptPath/ScriptName.log${default}"
|
||||
echo -e "${lit}\n-v, Verbose output from borgbackup${default}"
|
||||
echo -e "By default, this script will only log summary data from borg."
|
||||
echo -e "If you need/want more detailed information, the verbose setting"
|
||||
echo -e "will list every file processed along with their status. Note: Your"
|
||||
echo -e "log file can quickly get very very large using this option!"
|
||||
echo -e "${info}Default: NOT activated (standard logging)${default}"
|
||||
echo -e "${lit}\n-w, webserver's webroot directory${default}"
|
||||
echo -e "This is the location from which your webserver (NGINX, Apache,"
|
||||
echo -e "etc.) physically stores files to be served. This is NOT the"
|
||||
echo -e "configuration directory for your webserver! It is the place"
|
||||
echo -e "where the actual HTML/PHP/CSS/JS/etc. files are stored."
|
||||
echo -e "NOTE: If you omit this option, then the entire 503 copy process"
|
||||
echo -e "will be skipped regardless of the presence of a 503.html file."
|
||||
echo -e "If you don't want to use the 503 feature, omitting this is an easy"
|
||||
echo -e "way to skip it!"
|
||||
echo -e "${info}Default: NONE${default}"
|
||||
echo -e "${lit}\n-?, This help screen${default}\n"
|
||||
echo -e "${bold}Please refer to the readme file and/or ${lit}https://mytechiethoughts.com${default}"
|
||||
echo -e "for more information on this script.${normal}\n"
|
||||
# exit with code 1 -- there is no use logging this
|
||||
exit 1
|
||||
}
|
||||
|
||||
### Generate dynamic timestamp
|
||||
function stamp {
|
||||
echo `date +%F" "%T`
|
||||
}
|
||||
|
||||
### quit -- exit the script after logging any errors, warnings, etc.
|
||||
function quit {
|
||||
# list generated warnings, if any
|
||||
if [ ${#exitWarn[@]} -gt 0 ]; then
|
||||
echo -e "\n${warn}${scriptName} generated the following warnings:" \
|
||||
"${normal}" >> "$logFile"
|
||||
for warnCode in "${exitWarn[@]}"; do
|
||||
warnStamp="${warnCode%%_*}"
|
||||
warnValue="${warnCode##*_}"
|
||||
echo -e "${warn}${warnStamp} -- [WARNING]" \
|
||||
"${warningExplain[$warnValue]} (code: ${warnValue}) --" \
|
||||
"${normal}" >> "$logFile"
|
||||
done
|
||||
fi
|
||||
if [ -z "${exitError}" ]; then
|
||||
# exit cleanly
|
||||
echo -e "${note}[$(stamp)] -- ${scriptName} completed" \
|
||||
"--${normal}" >> "$logFile"
|
||||
exit 0
|
||||
else
|
||||
# list generated errors and explanations then exit script with code 2
|
||||
echo -e "\n${err}${scriptName} generated the following errors:" \
|
||||
"${normal}" >> "$logFile"
|
||||
for errCode in "${exitError[@]}"; do
|
||||
errStamp="${errCode%%_*}"
|
||||
errValue="${errCode##*_}"
|
||||
echo -e "${err}${errStamp} -- [ERROR] ${errorExplain[$errValue]}" \
|
||||
"(code: ${errValue}) --${normal}" >> "$logFile"
|
||||
done
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
function checkExist {
|
||||
if [ "$1" = "ff" ]; then
|
||||
# find file
|
||||
if [ -f "$2" ]; then
|
||||
# found
|
||||
return 0
|
||||
else
|
||||
# not found
|
||||
return 1
|
||||
fi
|
||||
elif [ "$1" = "fs" ]; then
|
||||
# find file > 0 bytes
|
||||
if [ -s "$2" ]; then
|
||||
# found
|
||||
return 0
|
||||
else
|
||||
# not found
|
||||
return 1
|
||||
fi
|
||||
elif [ "$1" = "fd" ]; then
|
||||
# find directory
|
||||
if [ -d "$2" ]; then
|
||||
# found
|
||||
return 0
|
||||
else
|
||||
# not found
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
### cleanup - cleanup files and directories created by this script
|
||||
function cleanup {
|
||||
## remove SQL dump file and directory
|
||||
rm -rf "$sqlDumpDir" >> "$logFile" 2>&1
|
||||
# verify directory is gone
|
||||
checkExist fd "$sqlDumpDir"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
# directory still exists
|
||||
exitWarn+=("[$(stamp)]_111")
|
||||
else
|
||||
# directory removed
|
||||
echo -e "${op}[$(stamp)] Removed SQL temp directory${normal}" \
|
||||
>> "$logFile"
|
||||
fi
|
||||
|
||||
## remove 503 error page
|
||||
# check value of 'clean503' to see if this is necessary (=1) otherwise, skip
|
||||
if [ "$clean503" = "1" ]; then
|
||||
# proceed with cleanup
|
||||
echo -e "${op}[$(stamp)] Removing 503 error page..." >> "$logFile"
|
||||
rm -f "$webroot/$err503File" >> "$logFile" 2>&1
|
||||
# verify file is actually gone
|
||||
checkExist ff "$webroot/$err503File"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
# file still exists
|
||||
exitWarn+=("[$(stamp)]_5030")
|
||||
else
|
||||
# file removed
|
||||
echo -e "${info}[$(stamp)] -- [INFO] 503 page removed from webroot" \
|
||||
"--${normal}" >> "$logFile"
|
||||
fi
|
||||
else
|
||||
echo -e "${op}[$(stamp)] 503 error page never copied to webroot," \
|
||||
"nothing to cleanup" >> "$logFile"
|
||||
fi
|
||||
|
||||
## restart mailflow docker containers
|
||||
# start and verify postfix
|
||||
operateDocker start postfix
|
||||
if [ "$dockerResultState" = "true" ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] Postfix container is running --" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_103")
|
||||
fi
|
||||
# start and verify dovecot
|
||||
operateDocker start dovecot
|
||||
if [ "$dockerResultState" = "true" ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] Dovecot container is running --" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_104")
|
||||
fi
|
||||
}
|
||||
|
||||
### operate docker containers
|
||||
function operateDocker {
|
||||
containerName="$(docker ps -a --format '{{ .Names }}' --filter name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1)"
|
||||
|
||||
# determine action to take
|
||||
if [ "$1" = "stop" ]; then
|
||||
echo -e "${op}[$(stamp)] Stopping ${2}-mailcow container...${normal}" \
|
||||
>> "$logFile"
|
||||
docker-compose stop --timeout ${dockerStopTimeout} ${2}-mailcow \
|
||||
2>> "$logFile"
|
||||
# verify container stopped (should return true)
|
||||
dockerResultState="$(docker inspect -f '{{ .State.Running }}' $containerName)"
|
||||
# verify clean stop (exit code 0)
|
||||
dockerResultExit="$(docker inspect -f '{{ .State.ExitCode }}' $containerName)"
|
||||
elif [ "$1" = "start" ]; then
|
||||
echo -e "${op}[$(stamp)] Starting ${2}-mailcow container...${normal}" \
|
||||
>> "$logFile"
|
||||
docker-compose start ${2}-mailcow 2>> "$logFile"
|
||||
# verify
|
||||
dockerResultState="$(docker inspect -f '{{ .State.Running }}' $containerName)"
|
||||
fi
|
||||
}
|
||||
|
||||
### End of Functions ###
|
||||
|
||||
|
||||
### Default parameters
|
||||
|
||||
# store the logfile in the same directory as this script using the script's name
|
||||
# with the extension .log
|
||||
scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
scriptName="$( basename ${0} )"
|
||||
logFile="$scriptPath/${scriptName%.*}.log"
|
||||
|
||||
# Set default docker-compose filename
|
||||
dockerComposeFile=docker-compose.yml
|
||||
|
||||
# set default 503 error page name and location in scriptPath
|
||||
err503Path="$scriptPath/503.html"
|
||||
err503File="${err503Path##*/}"
|
||||
|
||||
# Set default docker-compose stop timeout
|
||||
dockerStopTimeout=120
|
||||
|
||||
# Set default docker-compose start timeout
|
||||
dockerStartTimeout=180
|
||||
|
||||
# set default borgDetails path to scriptPath
|
||||
borgDetails="$scriptPath/mc_borg.details"
|
||||
|
||||
# set borg parameters to 'normal' verbosity
|
||||
borgCreateParams='--stats'
|
||||
borgPruneParams='--list'
|
||||
|
||||
|
||||
### Set script parameters to null and initialize array variables
|
||||
unset mailcowConfigFilePath
|
||||
unset mailcowPath
|
||||
unset dockerComposeFilePath
|
||||
unset PARAMS
|
||||
unset sqlDumpDir
|
||||
unset webroot
|
||||
unset clean503
|
||||
unset borgXtra
|
||||
unset borgExclude
|
||||
unset borgPrune
|
||||
unset BORG_BASE_DIR
|
||||
unset BORG_RSH
|
||||
unset BORG_REPO
|
||||
unset BORG_PASSPHRASE
|
||||
unset BORG_REMOTE_PATH
|
||||
unset TMPDIR
|
||||
unset dockerVolumeMail
|
||||
unset dockerVolumeRspamd
|
||||
unset dockerVolumePostfix
|
||||
unset dockerVolumeRedis
|
||||
unset dockerVolumeCrypt
|
||||
exitError=()
|
||||
errorExplain=()
|
||||
exitWarn=()
|
||||
warningExplain=()
|
||||
borgConfig=()
|
||||
xtraFiles=()
|
||||
|
||||
### Error codes
|
||||
errorExplain[101]="Could not stop Postfix container. Please check docker logs"
|
||||
errorExplain[102]="Could not stop Dovecot container. Please check docker logs"
|
||||
errorExplain[103]="Could not start Postfix container. Please check docker logs"
|
||||
errorExplain[104]="Could not start Dovecot container. Please check docker logs"
|
||||
errorExplain[201]="There was a problem dumping the SQL database. It has NOT been backed up"
|
||||
errorExplain[202]="There was a problem saving redis state information. It has NOT been backed up"
|
||||
errorExplain[210]="Invalid or non-existant borg base directory specified (borg backup details file)"
|
||||
errorExplain[211]="Invalid or non-existant path to borg SSH keyfile (borg backup details file)"
|
||||
errorExplain[212]="Name of borg repo was not specified (borg backup details file)"
|
||||
errorExplain[215]="Could not find/create 'tmp' directory within borg base directory. Please manually create it and ensure it's writable"
|
||||
errorExplain[220]="Borg exited with a critical error. Please check this script's logfile for details"
|
||||
errorExplain[221]="Borg prune exited with ERRORS. Please check this script's logfile for details"
|
||||
|
||||
|
||||
### Warning codes & messages
|
||||
warningExplain[111]="Could not remove SQL dump file and directory, please remove manually"
|
||||
warningExplain[5030]="Could not remove 503 error page. This MUST be removed manually before NGINX will serve webclients!"
|
||||
warningExplain[5031]="No webroot path was specified (-w parameter missing)"
|
||||
warningExplain[5032]="The specified webroot (-w parameter) could not be found"
|
||||
warningExplain[5033]="No 503 error page could be found. If not using the default located in the script directory, then check your -5 parameter"
|
||||
warningExplain[5035]="Error copying 503 error page to webroot"
|
||||
warn503="Web users will NOT be informed the server is down!"
|
||||
warningExplain[2111]="No password used for SSH keys or access to remote borg repo. This is an insecure configuration"
|
||||
warningExplain[2112]="No remote borg instance specified. Operations will be slower in this configuration"
|
||||
warningExplain[2113]="The specified file containing extra files for inclusion in borgbackup could not be found"
|
||||
warningExplain[2114]="The specified file containing exclusion patterns for borgbackup could not be found. Backup was performed as though NO exclusions were defined"
|
||||
warningExplain[2115]="No paramters provided for borg prune. No repo pruning has taken place. You should reconsider this decision to control the size/history of your backups"
|
||||
warningExplain[2116]="No additional locations are specified for inclusion in backup. ONLY Mailcow data and config files will be backed up (NO system files, etc). If this is unintentional, check the inclusion file referenced in your borgbackup settings"
|
||||
warningExplain[2200]="Borg completed with warnings. Please check this script's logfile for details"
|
||||
warningExplain[2201]="Borg exited with an unknown return-code. Please check this script's logfile for details"
|
||||
warningExplain[2210]="Borg prune exited with warnings. Please check this script's logfile for details"
|
||||
warningExplain[2212]="Borg prune exited with an unknown return-code. Please check this script's logfile for details"
|
||||
|
||||
|
||||
### Process script parameters
|
||||
|
||||
# If parameters are provided but don't start with '-' then show the help page
|
||||
# and exit with an error
|
||||
if [ -n "$1" ] && [[ ! "$1" =~ ^- ]]; then
|
||||
# show script help page
|
||||
scriptHelp
|
||||
fi
|
||||
|
||||
# use GetOpts to process parameters
|
||||
while getopts ':l:v5:w:b:m:d:1:2:' PARAMS; do
|
||||
case "$PARAMS" in
|
||||
l)
|
||||
# use provided location for logFile
|
||||
logFile="${OPTARG%/}"
|
||||
;;
|
||||
v)
|
||||
# verbose output from Borg
|
||||
borgCreateParams='--list --stats'
|
||||
borgPruneParams='--list'
|
||||
;;
|
||||
5)
|
||||
# Full path to 503 error page
|
||||
err503Path="${OPTARG%/}"
|
||||
err503File="${err503Path##*/}"
|
||||
;;
|
||||
w)
|
||||
# path to webserver webroot to copy 503 error page
|
||||
webroot="${OPTARG%/}"
|
||||
;;
|
||||
b)
|
||||
# path to file containing borgbackup settings and details
|
||||
borgDetails="${OPTARG%/}"
|
||||
;;
|
||||
m)
|
||||
# full path to mailcow.conf configuration file
|
||||
mailcowConfigFilePath="${OPTARG%/}"
|
||||
;;
|
||||
d)
|
||||
# name of docker-compose configuration file
|
||||
dockerComposeFile="${OPTARG}"
|
||||
;;
|
||||
1)
|
||||
# docker-compose stop timeout in seconds
|
||||
dockerStopTimeout="${OPTARG}"
|
||||
;;
|
||||
2)
|
||||
# docker-compose start timeout in seconds
|
||||
dockerStartTimeout="${OPTARG}"
|
||||
;;
|
||||
?)
|
||||
# unrecognized parameters trigger scriptHelp
|
||||
scriptHelp
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
### Verify script pre-requisties
|
||||
|
||||
## If not running as root, display error on console and exit
|
||||
if [ $(id -u) -ne 0 ]; then
|
||||
echo -e "\n${err}This script MUST be run as ROOT. Exiting.${normal}"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
## verify mailcow.conf location provided
|
||||
if [ -z "$mailcowConfigFilePath" ]; then
|
||||
echo -e "\n${err}You MUST provide the full path to your mailcow.conf" \
|
||||
"configuration file. Exiting.${normal}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## verify mailcow.conf and extract path
|
||||
if checkExist ff "$mailcowConfigFilePath"; then
|
||||
# extract directory name
|
||||
case $mailcowConfigFilePath in
|
||||
*/*)
|
||||
mailcowPath=${mailcowConfigFilePath%/*}
|
||||
;;
|
||||
*)
|
||||
mailcowPath="."
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo -e "\n${err}Could not locate the specified mailcow configuration" \
|
||||
"file: ${lit}${mailcowConfigFilePath}${normal}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# verify docker-compose.yml exists at location of mailcow.conf (standard setup)
|
||||
dockerComposeFilePath="${mailcowPath}/${dockerComposeFile}"
|
||||
if ! checkExist ff "$dockerComposeFilePath"; then
|
||||
echo -e "\n${err}Could not locate docker-compose configuration file:" \
|
||||
"${lit}${dockerComposeFilePath}${normal}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Ensure borgDetails file exists
|
||||
checkExist ff "$borgDetails"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "1" ]; then
|
||||
# sqlDetails file cannot be found
|
||||
echo -e "\n${err}The file containing your borgbackup details does not" \
|
||||
"exist (-b parameter)${normal}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
### Log start of script operations
|
||||
echo -e "${note}[$(stamp)] --- Start $scriptName execution ---${normal}" \
|
||||
>> "$logFile"
|
||||
echo -e "${info}[$(stamp)] -- [INFO] using ${lit}${mailcowConfigFilePath}" \
|
||||
>> "$logFile"
|
||||
echo -e "${info}[$(stamp)] -- [INFO] using ${lit}${dockerComposeFilePath}" \
|
||||
>> "$logFile"
|
||||
|
||||
|
||||
### Import additional variables from mailcow configuration file
|
||||
source "$mailcowConfigFilePath"
|
||||
|
||||
### Export PATH so this script can access all docker and docker-compose commands
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
### Export logFile variable for use by Borg
|
||||
export logFile="$logFile"
|
||||
|
||||
### Export docker container startup timeout variable
|
||||
export COMPOSE_HTTP_TIMEOUT=${dockerStartTimeout}
|
||||
|
||||
## Get docker volume paths on filesystem
|
||||
dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1)
|
||||
dockerVolumeRspamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_rspamd-vol-1)
|
||||
dockerVolumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_postfix-vol-1)
|
||||
dockerVolumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_redis-vol-1)
|
||||
dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1)
|
||||
|
||||
|
||||
### Create sqlDump temporary directory and sqlDumpFile name
|
||||
sqlDumpDir=$( mktemp -d )
|
||||
sqlDumpFile="backup-`date +%Y%m%d_%H%M%S`.sql"
|
||||
echo -e "${info}[$(stamp)] -- [INFO] mySQL dump file will be stored" \
|
||||
"at: ${lit}${sqlDumpDir}/${sqlDumpFile}${normal}" >> "$logFile"
|
||||
|
||||
|
||||
### 503 error page: If you dont' plan on using the auto-copied 503 then comment
|
||||
### this entire section starting with '--- Begin 503 section ---' until
|
||||
### '--- End 503 section ---' to suppress generated warnings
|
||||
|
||||
### --- Begin 503 section ---
|
||||
|
||||
## Check if webroot has been specified, if not, skip this entire section since there is nowhere to copy the 503 file.
|
||||
if [ -z "$webroot" ]; then
|
||||
# no webroot path provided
|
||||
echo -e "${info}[$(stamp)] -- [INFO] ${warn503} --${normal}" \
|
||||
>> "$logFile"
|
||||
exitWarn+=("[$(stamp)]_5031")
|
||||
clean503=0
|
||||
else
|
||||
# verify webroot actually exists
|
||||
checkExist fd "$webroot"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "1" ]; then
|
||||
# webroot directory specified could not be found
|
||||
echo -e "${info}[$(stamp)] -- [INFO] ${warn503} --${normal}" \
|
||||
>> "$logFile"
|
||||
exitWarn+=("[$(stamp)]_5032")
|
||||
clean503=0
|
||||
else
|
||||
# webroot exists
|
||||
echo -e "${op}[$(stamp)] Using webroot: ${lit}${webroot}${normal}" \
|
||||
>> "$logFile"
|
||||
# Verify 503 file existance at given path
|
||||
checkExist ff "$err503Path"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "1" ]; then
|
||||
# 503 file could not be found
|
||||
echo -e "${info}[$(stamp)] -- [INFO] ${warn503} --${normal}" \
|
||||
>> "$logFile"
|
||||
exitWarn+=("[$(stamp)]_5033")
|
||||
clean503=0
|
||||
else
|
||||
# 503 file exists and webroot is valid. Let's copy it!
|
||||
echo -e "${op}[$(stamp)] ${err503File} found at ${lit}${err503Path}" \
|
||||
"${normal}" >> "$logFile"
|
||||
echo -e "${op}[$(stamp)] Copying 503 error page to webroot..." \
|
||||
"${normal}" >> "$logFile"
|
||||
cp "${err503Path}" "$webroot/" >> "$logFile" 2>&1
|
||||
copyResult="$?"
|
||||
# verify copy was successful
|
||||
if [ "$copyResult" = "1" ]; then
|
||||
# copy was unsuccessful
|
||||
echo -e "${info}[$(stamp)] -- [INFO] ${warn503} --${normal}" \
|
||||
>> "$logFile"
|
||||
exitWarn+=("[$(stamp)]_5035")
|
||||
clean503=0
|
||||
else
|
||||
# copy was successful
|
||||
echo -e "${info}[$(stamp)] -- [INFO] 503 error page" \
|
||||
"successfully copied to webroot --${normal}" >> "$logFile"
|
||||
clean503=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
### --- End 503 section ---
|
||||
|
||||
|
||||
### Change directory to mailcowPath
|
||||
cd "$mailcowPath"
|
||||
|
||||
|
||||
### Stop postfix and dovecot so mailflow is stopped until backup is completed
|
||||
## Stop postfix-mailcow container
|
||||
operateDocker stop postfix
|
||||
# process result
|
||||
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] Postfix container stopped --${normal}" \
|
||||
>> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_101")
|
||||
cleanup
|
||||
quit
|
||||
fi
|
||||
## Stop dovecot-mailcow container
|
||||
operateDocker stop dovecot
|
||||
# process result
|
||||
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] Dovecot container stopped --${normal}" \
|
||||
>> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_102")
|
||||
cleanup
|
||||
quit
|
||||
fi
|
||||
|
||||
|
||||
### Dump SQL
|
||||
echo -e "${op}[$(stamp)] Dumping mailcow SQL database...${normal}" >> "$logFile"
|
||||
docker-compose exec -T mysql-mailcow mysqldump --default-character-set=utf8mb4 \
|
||||
-u${DBUSER} -p${DBPASS} ${DBNAME} > "$sqlDumpDir/$sqlDumpFile" \
|
||||
2>> "$logFile"
|
||||
dumpResult=$(docker-compose exec -T mysql-mailcow echo "$?")
|
||||
## very mysqldump completed successfully
|
||||
if [ "$dumpResult" = "0" ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] mySQLdump completed successfully --" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_201")
|
||||
fi
|
||||
## verify the dump file was actually written to disk
|
||||
checkExist fs "$sqlDumpDir/$sqlDumpFile"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
echo -e "${ok}[$(stamp)] -- [SUCCESS] SQL successfully dumped --" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_201")
|
||||
fi
|
||||
|
||||
### Save redis state
|
||||
## Delete any existing redis dump file otherwise our file check will be useless
|
||||
echo -e "${op}[$(stamp)] Cleaning up old redis state backup...${normal}" \
|
||||
>> "$logFile"
|
||||
checkExist ff "$dockerVolumeRedis/dump.rdb"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
echo -e "${lit}[$(stamp)] Old redis backup found. ${op}Deleting..." \
|
||||
"${normal}" >> "$logFile"
|
||||
rm -f "$dockerVolumeRedis/dump.rdb" 2>> "$logFile"
|
||||
echo -e "${op}[$(stamp)] ...done${normal}" >> "$logFile"
|
||||
else
|
||||
echo -e "${op}[$(stamp)] No old redis backup found${normal}" \
|
||||
>> "$logFile"
|
||||
fi
|
||||
## Export redis
|
||||
echo -e "${op}[$(stamp)] Saving redis state information...${normal}" >> "$logFile"
|
||||
docker-compose exec -T redis-mailcow redis-cli save >> "$logFile" 2>&1
|
||||
saveResult=$(docker-compose exec -T redis-mailcow echo "$?")
|
||||
# verify save operation completed successfully
|
||||
if [ "$saveResult" = "0" ]; then
|
||||
echo -e "${info}[$(stamp)] -- [INFO] redis save-state successful --" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_202")
|
||||
fi
|
||||
## verify save-file written to disk
|
||||
checkExist fs "$dockerVolumeRedis/dump.rdb"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
echo -e "${ok}[$(stamp)] -- [SUCCESS] redis state saved --${normal}" \
|
||||
>> "$logFile"
|
||||
else
|
||||
exitError+=("[$(stamp)]_202")
|
||||
fi
|
||||
|
||||
|
||||
### Call borgbackup to copy actual files
|
||||
echo -e "${op}[$(stamp)] Pre-backup tasks completed, calling borgbackup..." \
|
||||
"${normal}" >> "$logFile"
|
||||
|
||||
## Get borgbackup settings and repo details
|
||||
# read definition file and map to array variable
|
||||
mapfile -t borgConfig < "$borgDetails"
|
||||
## check if any required borg configuration variables in defintion file are
|
||||
## empty and exit with error, otherwise, map array items to variables
|
||||
# check: borg base directory
|
||||
echo -e "${op}[$(stamp)] Verifying supplied borg configuration variables..." \
|
||||
"${normal}" >> "$logFile"
|
||||
if [ -z "${borgConfig[0]}" ]; then
|
||||
exitError+=("[$(stamp)]_210")
|
||||
cleanup
|
||||
quit
|
||||
else
|
||||
# verify the path actually exists
|
||||
checkExist fd "${borgConfig[0]}"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "1" ]; then
|
||||
# borg base directory specified could not be found
|
||||
exitError+=("[$(stamp)]_210")
|
||||
cleanup
|
||||
quit
|
||||
fi
|
||||
echo -e "${op}[$(stamp)] Borg base dir... OK${normal}" >> "$logFile"
|
||||
export BORG_BASE_DIR="${borgConfig[0]%/}"
|
||||
fi
|
||||
# check: path to SSH keyfile
|
||||
if [ -z "${borgConfig[1]}" ]; then
|
||||
exitError+=("[$(stamp)]_211")
|
||||
cleanup
|
||||
quit
|
||||
else
|
||||
checkExist ff "${borgConfig[1]}"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = 1 ]; then
|
||||
# SSH keyfile specified could not be found
|
||||
exitError+=("[$(stamp)]_211")
|
||||
cleanup
|
||||
quit
|
||||
fi
|
||||
echo -e "${op}[$(stamp)] Borg SSH key... OK${normal}" >> "$logFile"
|
||||
export BORG_RSH="ssh -i ${borgConfig[1]}"
|
||||
fi
|
||||
# check: name of borg repo
|
||||
if [ -z "${borgConfig[2]}" ]; then
|
||||
exitError+=("[$(stamp)]_212")
|
||||
cleanup
|
||||
quit
|
||||
else
|
||||
echo -e "${op}[$(stamp)] Borg REPO name... OK${normal}" >> "$logFile"
|
||||
export BORG_REPO="${borgConfig[2]}"
|
||||
fi
|
||||
# repo password
|
||||
if [ -n "${borgConfig[3]}" ]; then
|
||||
echo -e "${op}[$(stamp)] Borg SSH/REPO password... OK${normal}" >> "$logFile"
|
||||
export BORG_PASSPHRASE="${borgConfig[3]}"
|
||||
else
|
||||
exitWarn+=("[$(stamp)]_2111")
|
||||
# if the password was omitted by mistake, export a dummy password so borg
|
||||
# fails with an error instead of sitting and waiting for input
|
||||
export BORG_PASSPHRASE="DummyPasswordSoBorgFails"
|
||||
fi
|
||||
# additional files to be backed up
|
||||
borgXtra="${borgConfig[4]}"
|
||||
# file with pattern definition for excluded files
|
||||
borgExclude="${borgConfig[5]}"
|
||||
# parameters for borg prune
|
||||
borgPrune="${borgConfig[6]}"
|
||||
# export: borg remote path (if not blank)
|
||||
if [ -n "${borgConfig[7]}" ]; then
|
||||
echo -e "${op}[$(stamp)] Borg REMOTE path... OK${normal}" >> "$logFile"
|
||||
export BORG_REMOTE_PATH="${borgConfig[7]}"
|
||||
else
|
||||
exitWarn+=("[$(stamp)]_2112")
|
||||
fi
|
||||
|
||||
## If borgXtra exists, map contents to an array variable
|
||||
if [ -n "$borgXtra" ]; then
|
||||
echo -e "${op}[$(stamp)] Processing referenced extra files list for" \
|
||||
"borgbackup to include in backup${normal}" >> "$logFile"
|
||||
checkExist ff "$borgXtra"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
echo -e "${op}[$(stamp)] Found ${lit}${borgXtra}${normal}" >> "$logFile"
|
||||
mapfile -t xtraFiles < "$borgXtra"
|
||||
echo -e "${op}[$(stamp)] Processed extra files list for inclusion in" \
|
||||
"borgbackup${normal}" >> "$logFile"
|
||||
else
|
||||
exitWarn+=("[$(stamp)]_2113")
|
||||
fi
|
||||
else
|
||||
# no extra locations specified
|
||||
echo -e "${op}[$(stamp)] No additional locations specified for backup." \
|
||||
"Only Mailcow data and config files will be backed up.${normal}" \
|
||||
>> "$logFile"
|
||||
exitWarn+=("[$(stamp)]_2116")
|
||||
fi
|
||||
|
||||
## Check if borgExclude exists since borg will throw an error if it's missing
|
||||
if [ -n "$borgExclude" ]; then
|
||||
checkExist ff "$borgExclude"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
echo -e "${op}[$(stamp)] Found ${lit}${borgExclude}${normal}" \
|
||||
>> "$logFile"
|
||||
else
|
||||
# file not found, unset the variable so it's like it was not specified
|
||||
# in the first place and continue with backup
|
||||
unset borgExclude
|
||||
exitWarn+=("[$(stamp)]_2114")
|
||||
fi
|
||||
else
|
||||
echo -e "${op}[$(stamp)] Exclusion pattern file not specified." \
|
||||
"No exclusions will be processed${normal}" >> "$logFile"
|
||||
fi
|
||||
|
||||
|
||||
## Export TMPDIR environment variable for borg via python
|
||||
## 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
|
||||
echo -e "${op}[$(stamp)] Checking for tmp directory at ${lit}${BORG_BASE_DIR}" \
|
||||
"${normal}" >> "$logFile"
|
||||
checkExist fd "$BORG_BASE_DIR/tmp"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "1" ]; then
|
||||
# folder not found
|
||||
echo -e "${op}[$(stamp)] tmp folder not found... creating${lit}" \
|
||||
"${BORG_BASE_DIR}/tmp${normal}" >> "$logFile"
|
||||
mkdir "$BORG_BASE_DIR/tmp" 2>> "$logFile"
|
||||
# verify folder created
|
||||
checkExist fd "$BORG_BASE_DIR/tmp"
|
||||
checkResult="$?"
|
||||
if [ "$checkResult" = "0" ]; then
|
||||
# folder exists
|
||||
echo -e "${op}[$(stamp)] tmp folder created within borg base directory" \
|
||||
"${normal}" >> "$logFile"
|
||||
else
|
||||
# problem creating folder and script will exit
|
||||
exitError+=("[$(stamp)]_215")
|
||||
cleanup
|
||||
quit
|
||||
fi
|
||||
else
|
||||
# folder found
|
||||
echo -e "${op}[$(stamp)] tmp folder found within borg base directory" \
|
||||
"${normal}" >> "$logFile"
|
||||
fi
|
||||
# export TMPDIR environment variable
|
||||
export TMPDIR="${BORG_BASE_DIR}/tmp"
|
||||
|
||||
|
||||
## Generate and execute borg
|
||||
# commandline depends on whether borgExclude is empty or not
|
||||
if [ -z "$borgExclude" ]; then
|
||||
# borgExclude is empty
|
||||
echo -e "${bold}${op}[$(stamp)] Executing borg without exclusions${normal}" \
|
||||
>> "$logFile"
|
||||
borg --show-rc create ${borgCreateParams} ::`date +%Y-%m-%d_%H%M%S` \
|
||||
"${xtraFiles[@]}" \
|
||||
"${sqlDumpDir}" \
|
||||
"${dockerVolumeMail}" "${dockerVolumeRspamd}" "${dockerVolumePostfix}" \
|
||||
"${dockerVolumeRedis}" "${dockerVolumeCrypt}" \
|
||||
2>> "$logFile"
|
||||
else
|
||||
# borgExclude is not empty
|
||||
echo -e "${bold}${op}[$(stamp)] Executing borg with exclusions${normal}" \
|
||||
>> "$logFile"
|
||||
borg --show-rc create ${borgCreateParams} --exclude-from "${borgExclude}" \
|
||||
::`date +%Y-%m-%d_%H%M%S` \
|
||||
"${xtraFiles[@]}" \
|
||||
"${sqlDumpDir}" \
|
||||
"${dockerVolumeMail}" "${dockerVolumeRspamd}" "${dockerVolumePostfix}" \
|
||||
"${dockerVolumeRedis}" "${dockerVolumeCrypt}" \
|
||||
2>> "$logFile"
|
||||
fi
|
||||
|
||||
## Check status of borg operation
|
||||
borgResult="$?"
|
||||
if [ "$borgResult" -eq 0 ]; then
|
||||
echo -e "${ok}[$(stamp)] -- [SUCCESS] Borg backup completed successfully --" \
|
||||
"${normal}" >> "$logFile"
|
||||
elif [ "$borgResult" -eq 1 ]; then
|
||||
exitWarn+=("[$(stamp)]_2200")
|
||||
elif [ "$borgResult" -ge 2 ]; then
|
||||
exitError+=("[$(stamp)]_220")
|
||||
cleanup
|
||||
quit
|
||||
else
|
||||
exitWarn+=("[$(stamp)]_2201")
|
||||
fi
|
||||
|
||||
## Generate and execute borg prune
|
||||
# command depends on whether or not parameters have been defined
|
||||
if [ -n "$borgPrune" ]; then
|
||||
# parameters defined
|
||||
echo -e "${bold}${op}[$(stamp)] Executing borg prune operation${normal}" \
|
||||
>> "$logFile"
|
||||
borg prune --show-rc -v ${borgPruneParams} ${borgPrune} \
|
||||
2>> "$logFile"
|
||||
# check return-status
|
||||
pruneResult="$?"
|
||||
if [ "$pruneResult" -eq 0 ]; then
|
||||
echo -e "${ok}[$(stamp)] -- [SUCCESS] Borg prune completed successfully" \
|
||||
"--${normal}" >> "$logFile"
|
||||
elif [ "$pruneResult" -eq 1 ]; then
|
||||
exitWarn+=("[$(stamp)]_2210")
|
||||
elif [ "$pruneResult" -ge 2 ]; then
|
||||
exitError+=("[$(stamp)]_221")
|
||||
else
|
||||
exitWarn+=("[$(stamp)]_2212")
|
||||
fi
|
||||
else
|
||||
# parameters not defined... skip pruning
|
||||
exitWarn+=("[$(stamp)]_2115")
|
||||
fi
|
||||
|
||||
|
||||
### borgbackup completed
|
||||
echo -e "${op}[$(stamp)] Borgbackup completed... begin cleanup" \
|
||||
"${normal}" >> "$logFile"
|
||||
|
||||
|
||||
### Exit script
|
||||
echo -e "${bold}${op}[$(stamp)] ***Normal exit process***${normal}" \
|
||||
>> "$logFile"
|
||||
cleanup
|
||||
echo -e "${bold}${ok}[$(stamp)] -- [SUCCESS] All processes completed" \
|
||||
"successfully --${normal}" >> "$logFile"
|
||||
quit
|
||||
|
||||
# This code should not be executed since the 'quit' function should terminate
|
||||
# this script. Therefore, exit with code 99 if we get to this point.
|
||||
exit 99
|
@ -1,8 +0,0 @@
|
||||
<path to borgbackup base directory> /var/borgbackup
|
||||
<path to SSH private key for remote server> /var/borgbackup/sshPrivate.key
|
||||
<connection string to remote repo> user@servername.tld:repoName/
|
||||
<password for repo> pAsSwOrd
|
||||
<path to file listing extra files> /root/scripts/xtraLocations.borg
|
||||
<path to file with exclusions> /root/scripts/excludeLocations.borg
|
||||
<purge timeframe options> --keep-within=7d --keep-daily=30 --keep-weekly=12 --keep-monthly=-1
|
||||
<location of borg remote instance> borg1
|
@ -1,27 +0,0 @@
|
||||
/etc/network/interfaces
|
||||
/etc/network/interfaces.d/
|
||||
/etc/systemd/timesyncd.conf
|
||||
/etc/profile
|
||||
/etc/bash.bashrc
|
||||
/etc/skel/
|
||||
/etc/nanorc
|
||||
/etc/msmtprc
|
||||
/etc/msmtp_aliases
|
||||
/etc/apt/sources.list
|
||||
/etc/apt/sources.list.d/
|
||||
/etc/apt/listchanges.conf
|
||||
/etc/apt/apt.conf.d/50unattended-upgrades
|
||||
/etc/apt/apt.conf.d/20auto-upgrades
|
||||
/etc/ssh/
|
||||
/etc/logwatch/
|
||||
/etc/letsencrypt/
|
||||
/etc/fail2ban/fail2ban.local
|
||||
/etc/fail2ban/jail.local
|
||||
/etc/fail2ban/action.d/ufw.conf
|
||||
/etc/fail2ban/filter.d/ufw-probe.conf
|
||||
/etc/fail2ban/jail.d/
|
||||
/root/.bashrc
|
||||
/root/scripts/
|
||||
/root/.ssh/
|
||||
/etc/nginx/
|
||||
/usr/share/nginx/html/
|
Loading…
Reference in New Issue
Block a user