34 Commits

Author SHA1 Message Date
Asif Bacchus f2231c7074 fix(BACKUP): change borg cmd
- place common-options after borg command verb
- thanks to lluis

Closes github 7
2021-02-09 16:56:38 -07:00
Asif Bacchus 2747b10040 docs(RESTORE): remove misleading copied comments 2021-02-09 14:59:59 -07:00
Asif Bacchus 876f8eee9e fix(RESTORE): fix restore path when non-verbose
- non-verbose tar extraction was set to null path
2021-02-09 14:58:50 -07:00
Asif Bacchus 3a3b02685b make restore script executable 2021-02-09 08:30:06 -07:00
Asif Bacchus 85e34c73a3 feature(BACKUP): add compression option
- parameter to specify compression used by borg
- as requested by shaneonabike (via github)

Closes github 5
2021-02-09 08:22:48 -07:00
Asif Bacchus 9e592633b9 fix(BACKUP): bypass possible cd aliases 2021-02-09 08:03:23 -07:00
Asif Bacchus e127616800 fix(BACKUP): fix missing parameter shift
- docker timeout parameters
2021-02-09 08:02:34 -07:00
Asif Bacchus 053e45affc docs(RESTORE): add inline help 2021-02-09 07:46:42 -07:00
Asif Bacchus fb58c06489 fix(RESTORE): fix output to console when non-verbose 2021-02-09 07:13:12 -07:00
Asif Bacchus 010ab1dceb perf(RESTORE): exit if nothing to do 2021-02-09 06:49:33 -07:00
Asif Bacchus f28ad5e6e7 refactor(RESTORE): add logging for mailcow start/stop 2021-02-09 06:45:30 -07:00
Asif Bacchus 06df565348 style(RESTORE): add space before append redirects 2021-02-09 06:42:27 -07:00
Asif Bacchus ea0d98734a fix(RESTORE): fix accidental log file overwrite vs append 2021-02-09 06:39:38 -07:00
Asif Bacchus b07ad2c0ee perf(RESTORE): use docker-compose stop timeout 2021-02-09 06:01:04 -07:00
Asif Bacchus 84605567cd feature(RESTORE): restart mailcow after completion 2021-02-09 05:59:37 -07:00
Asif Bacchus 8c6019f7da feature(RESTORE): process secondary volume restoration 2021-02-09 05:48:17 -07:00
Asif Bacchus 363c970f2f refactor(RESTORE): create and use restore function
- single function to handle non-SQL restore operations
- replace previous mail and crypt restore with new function
2021-02-09 05:41:56 -07:00
Asif Bacchus cc30581382 feature(RESTORE): restore mail and encryption key 2021-02-09 05:00:08 -07:00
Asif Bacchus c9b85d084e feature(RESTORE): stop docker containers and confirm 2021-02-08 11:30:39 -07:00
Asif Bacchus bf18c80f94 refactor(RESTORE): execute docker-compose silently 2021-02-08 10:59:47 -07:00
Asif Bacchus 764f35a695 docs(RESTORE): update error message list 2021-02-08 10:53:14 -07:00
Asif Bacchus ba212f738f feature(RESTORE): clearer more accurate exit report messages
- change exit messages based on errors, warnings, etc.
- separate error codes for exit with non-crit err and/or warnings
2021-02-08 10:52:17 -07:00
Asif Bacchus 59cc9a5d66 fix(RESTORE): increment errorCount on SQL import error 2021-02-08 10:39:35 -07:00
Asif Bacchus 7b3ff7f6ae fix(RESTORE): strip trailing slash from backup-location 2021-02-08 10:31:03 -07:00
Asif Bacchus 78e1ed2539 perf(RESTORE): remove duplicate backup-location existence check 2021-02-08 10:28:21 -07:00
Asif Bacchus af4fcf3cf3 fix(RESTORE): exit on invalid backup location 2021-02-08 10:25:24 -07:00
Asif Bacchus 561437d667 fix(RESTORE): fix printf err 2021-02-08 10:24:19 -07:00
Asif Bacchus e4cd29fd42 feature(RESTORE): restore SQL database 2021-02-08 09:43:49 -07:00
Asif Bacchus 54ca63657f fix(RESTORE): change to mc directory before imports 2021-02-08 09:21:25 -07:00
Asif Bacchus de96d55268 refactor(RESTORE): update imported code to use writeLog func 2021-02-08 08:56:12 -07:00
Asif Bacchus 1d793dc291 feature(RESTORE): abort if mc not pre-initialized 2021-02-08 08:46:14 -07:00
Asif Bacchus 7ce419d3eb struct(RESTORE): remove borg integration
- require backup already downloaded/copied on local system
- removes need for borg
- far reduced complexity
2021-02-08 08:15:46 -07:00
Asif Bacchus eadba0bee9 struct(RESTORE): add param processing 2021-02-08 08:06:41 -07:00
Asif Bacchus 9a363d8510 fix(RESTORE): move path to pre-flight
- export path so pre-flight checks are using consistent env
2021-02-08 07:11:47 -07:00
6 changed files with 491 additions and 164 deletions
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ContentModelUserStore">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RIDER_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../.." />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+3
View File
@@ -7,4 +7,7 @@
<inspection_tool class="SubjectLimit" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="SubjectLimit" enabled="true" level="ERROR" enabled_by_default="true" />
</profile> </profile>
</component> </component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project> </project>
+22 -4
View File
@@ -164,6 +164,10 @@ scriptHelp() {
textblock "Path to write log file" textblock "Path to write log file"
defaultsTextblock "(scriptPath/scriptName.log)" defaultsTextblock "(scriptPath/scriptName.log)"
newline newline
switchTextblock "--compression"
textblock "Compression algorithm(s) that borg should use. Please run 'borg help compression' for details."
defaultsTextblock "(not specified, use borg default of lz4)"
newline
switchTextblock "[SWITCH] -v | --verbose" switchTextblock "[SWITCH] -v | --verbose"
textblock "Log borg output with increased verbosity (list all files). Careful! Your log file can get very large very quickly!" 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)" defaultsTextblock "(normal output, option is OFF)"
@@ -300,6 +304,15 @@ while [ $# -gt 0 ]; do
badParam empty "$@" badParam empty "$@"
fi fi
;; ;;
--compression)
# set borg archive compression
if [ -n "$2" ]; then
borgCompression="$2"
shift
else
badParam empty "$@"
fi
;;
-v|--verbose) -v|--verbose)
# set verbose logging from borg # set verbose logging from borg
borgCreateParams='--list --stats' borgCreateParams='--list --stats'
@@ -367,6 +380,7 @@ while [ $# -gt 0 ]; do
badParam empty "$@" badParam empty "$@"
else else
dockerStartTimeout="$2" dockerStartTimeout="$2"
shift
fi fi
;; ;;
-t2|--timeout-stop) -t2|--timeout-stop)
@@ -374,6 +388,7 @@ while [ $# -gt 0 ]; do
badParam empty "$@" badParam empty "$@"
else else
dockerStopTimeout="$2" dockerStopTimeout="$2"
shift
fi fi
;; ;;
*) *)
@@ -417,7 +432,10 @@ fi
if [ ! -f "$mcDockerCompose" ]; then if [ ! -f "$mcDockerCompose" ]; then
badParam dne "(--docker-compose)" "$mcDockerCompose" badParam dne "(--docker-compose)" "$mcDockerCompose"
fi fi
# set compression level if specified
if [ -n "$borgCompression" ]; then
borgCreateParams="${borgCreateParams} --compression ${borgCompression}"
fi
### read mailcow.conf and set vars as needed ### read mailcow.conf and set vars as needed
# shellcheck source=./mailcow.conf.shellcheck # shellcheck source=./mailcow.conf.shellcheck
@@ -615,7 +633,7 @@ if [ "$use503" -eq 1 ]; then
fi fi
### change to mailcow directory so docker commands execute properly ### change to mailcow directory so docker commands execute properly
cd "${mcConfig%/*}" || exitError 100 'Could not change to mailcow directory.' \cd "${mcConfig%/*}" || exitError 100 'Could not change to mailcow directory.'
### stop postfix and dovecot mail containers to prevent mailflow during backup ### stop postfix and dovecot mail containers to prevent mailflow during backup
doDocker stop postfix doDocker stop postfix
@@ -672,7 +690,7 @@ printf "%s[%s] -- [INFO] Pre-backup tasks completed, calling borgbackup --%s\n"
## construct the proper borg commandline ## construct the proper borg commandline
# base command # base command
if [ "$exclusions" -eq 0 ]; then if [ "$exclusions" -eq 0 ]; then
borgCMD="borg --show-rc create ${borgCreateParams} \ borgCMD="borg create --show-rc ${borgCreateParams} \
::$(date +%Y-%m-%d_%H%M%S) \ ::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \ ${mcConfig%/*} \
${sqlDumpDir} \ ${sqlDumpDir} \
@@ -683,7 +701,7 @@ if [ "$exclusions" -eq 0 ]; then
${dockerVolumeCrypt} \ ${dockerVolumeCrypt} \
${xtraList}" ${xtraList}"
elif [ "$exclusions" -eq 1 ]; then elif [ "$exclusions" -eq 1 ]; then
borgCMD="borg --show-rc create ${borgCreateParams} \ borgCMD="borg create --show-rc ${borgCreateParams} \
--exclude-from ${borgExcludeListPath} \ --exclude-from ${borgExcludeListPath} \
::$(date +%Y-%m-%d_%H%M%S) \ ::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \ ${mcConfig%/*} \
Regular → Executable
+446 -160
View File
@@ -5,7 +5,7 @@
### this assumes three things: ### this assumes three things:
### 1. standard mailcow-dockerized setup as per the docs ### 1. standard mailcow-dockerized setup as per the docs
### 2. backups made using the backup script from this git repo ### 2. backups made using the backup script from this git repo
### 3. backups successfully written to your borg repo ### 3. backups already downloaded from your borg repo
####### #######
### text-formatting presets ### text-formatting presets
@@ -36,18 +36,6 @@ trap trapExit 1 2 3 6
### functions ### functions
badDetails() {
if [ "$1" = 'empty' ]; then
writeLog 'done' 'error'
writeLog 'error' '10' "details:${2} cannot be blank/empty."
exitError 130
elif [ "$1" = 'dne' ]; then
writeLog 'done' 'error'
writeLog 'error' '11' "details:${2} file or directory does not exist."
exitError 131
fi
}
consoleError() { consoleError() {
printf "\n%s%s\n" "$err" "$2" printf "\n%s%s\n" "$err" "$2"
printf "Exiting.\n\n%s" "$norm" printf "Exiting.\n\n%s" "$norm"
@@ -55,15 +43,78 @@ consoleError() {
} }
exitError() { exitError() {
printf "%s[%s] --- %s execution completed with error ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile" printf "%s[%s] --- %s execution completed with error ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit "$1" exit "$1"
} }
doRestore() {
sourceFiles=$(find "${backupLocation}" -iname "${1}" -type d)
if [ -n "$sourceFiles" ]; then
if [ "$verbose" -eq 1 ]; then
if (! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) >> "$logfile" ); then
return 1
else
return 0
fi
else
if (! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) > /dev/null 2>&1 ); then
return 1
else
return 0
fi
fi
else
return 2
fi
}
scriptHelp() { scriptHelp() {
textNewline textNewline
printf "%sUsage: %s [parameters]%s\n\n" "$bold" "$scriptName" "$norm" printf "%sUsage: %s [parameters]%s\n\n" "$bold" "$scriptName" "$norm"
textblock "The only required parameter is -b | --backup-location."
textblock "If a parameter is not supplied, its default value will be used."
textblock "Switch parameters will only be activated if specified."
textblockHeader "script parameters"
textblockParam "-b | --backup-location"
textblock "Directory containing extracted backup files from borg repo. REQUIRED."
textNewline textNewline
textblock "If a parameter is not supplied, its default value will be used. Switch parameters will remain DEactivated if NOT specified." textblockParam "-l | --log" "scriptPath/scriptName.log"
textblock "Path to write log file. Best efforts will be made to create any specified paths."
textNewline
textblockParam "-v | --verbose" "false"
textblock "Enable verbose logging. This will list EVERY restored file possibly making your log file quite large! [SWITCH]"
textNewline
textblockParam "--skip-mail" "false"
textblock "Skip restoring mail and encryption key. [SWITCH]"
textNewline
textblockParam "--skip-sql" "false"
textblock "Skip restoring mailcow settings database. [SWITCH]"
textNewline
textblockParam "--skip-postfix" "false"
textblock "Skip restoring postfix settings. [SWITCH]"
textNewline
textblockParam "--skip-rspamd" "false"
textblock "Skip restoring Rspamd settings/configuration/history. [SWITCH]"
textNewline
textblockParam "--skip-redis" "false"
textblock "Skip restoring redis database. [SWITCH]"
textNewline
textblockParam "-? | -h | --help"
textblock "Display this help screen."
textblockHeader "mailcow parameters"
textblockParam "-d | --docker-compose" "/opt/mailcow-dockerized/docker-compose.yml"
textblock "FULL path to mailcow's 'docker-compose.yml' file."
textNewline
textblockParam "-m | --mailcow-config" "/opt/mailcow-dockerized/mailcow.conf"
textblock "FULL path to mailcow configuration file ('mailcow.conf'). The path of this file is also used to determine your mailcow directory."
textblockHeader "docker parameters"
textblockParam "-t1 | --timeout-start" "180"
textblock "Seconds to wait for docker containers to start."
textNewline
textblockParam "-t2 | --timeout-stop" "120"
textblock "Seconds to wait for docker containers to stop."
textNewline
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}"
textNewline textNewline
exit 0 exit 0
} }
@@ -77,7 +128,7 @@ textblock() {
} }
textblockHeader() { textblockHeader() {
printf "\n%s%s***%s***%s\n" "$bold" "$magenta" "$1" "$norm" printf "\n%s%s*** %s ***%s\n\n" "$bold" "$magenta" "$1" "$norm"
} }
textblockParam() { textblockParam() {
@@ -95,31 +146,30 @@ textNewline() {
} }
trapExit() { trapExit() {
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" "$err" "$(stamp)" "$norm" >>"$logfile" printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" "$err" "$(stamp)" "$norm" >> "$logfile"
cleanup printf "%s[%s] --- %s execution terminated via signal ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
printf "%s[%s] --- %s execution terminated via signal ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile"
exit 99 exit 99
} }
writeLog() { writeLog() {
if [ "$1" = "task" ]; then if [ "$1" = "task" ]; then
printf "%s[%s] -- [INFO] %s... " "$info" "$(stamp)" "$2" >>"$logfile" printf "%s[%s] -- [INFO] %s... " "$info" "$(stamp)" "$2" >> "$logfile"
elif [ "$1" = "done" ]; then elif [ "$1" = "done" ]; then
if [ -z "$2" ]; then if [ -z "$2" ]; then
printf "%sdone%s --\n%s" "$ok" "$info" "$norm" >>"$logfile" printf "%sdone%s --\n%s" "$ok" "$info" "$norm" >> "$logfile"
elif [ "$2" = "error" ]; then elif [ "$2" = "error" ]; then
printf "%sERROR%s --\n%s" "$err" "$info" "$norm" >>"$logfile" printf "%sERROR%s --\n%s" "$err" "$info" "$norm" >> "$logfile"
elif [ "$2" = "warn" ]; then elif [ "$2" = "warn" ]; then
printf "%swarning%s --\n%s" "$yellow" "$info" "$norm" >>"$logfile" printf "%swarning%s --\n%s" "$yellow" "$info" "$norm" >> "$logfile"
fi fi
elif [ "$1" = "error" ]; then elif [ "$1" = "error" ]; then
printf "%s[%s] -- [ERROR] %s: %s --\n%s" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logfile" printf "%s[%s] -- [ERROR] %s: %s --\n%s" "$err" "$(stamp)" "$2" "$3" "$norm" >> "$logfile"
elif [ "$1" = "warn" ]; then elif [ "$1" = "warn" ]; then
printf "%s[%s] -- [WARNING] %s --\n%s" "$yellow" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [WARNING] %s --\n%s" "$yellow" "$(stamp)" "$2" "$norm" >> "$logfile"
elif [ "$1" = "info" ]; then elif [ "$1" = "info" ]; then
printf "%s[%s] -- [INFO] %s --\n%s" "$info" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [INFO] %s --\n%s" "$info" "$(stamp)" "$2" "$norm" >> "$logfile"
elif [ "$1" = "success" ]; then elif [ "$1" = "success" ]; then
printf "%s[%s] -- [SUCCESS] %s --\n%s" "$ok" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [SUCCESS] %s --\n%s" "$ok" "$(stamp)" "$2" "$norm" >> "$logfile"
fi fi
} }
@@ -127,14 +177,22 @@ writeLog() {
# script related # script related
scriptPath="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)" scriptPath="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
scriptName="$(basename "$0")" scriptName="$(basename "$0")"
configDetails="$scriptPath/${scriptName%.*}.details"
errorCount=0 errorCount=0
warnCount=0 warnCount=0
backupLocation=""
sqlBackup=""
restoreMail=1
restoreSQL=1
restorePostfix=1
restoreRedis=1
restoreRspamd=1
verbose=0
# logfile default: same location and name as script but with '.log' extension # logfile default: same location and name as script but with '.log' extension
logfile="$scriptPath/${scriptName%.*}.log" logfile="$scriptPath/${scriptName%.*}.log"
# mailcow/docker related # mailcow/docker related
mcConfig='/opt/mailcow-dockerized/mailcow.conf' mcConfig='/opt/mailcow-dockerized/mailcow.conf'
mcDockerCompose='/opt/mailcow-dockerized/docker-compose.yml' mcDockerCompose='/opt/mailcow-dockerized/docker-compose.yml'
sqlRunning=0
dockerStartTimeout=180 dockerStartTimeout=180
dockerStopTimeout=120 dockerStopTimeout=120
@@ -146,12 +204,92 @@ fi
### process startup parameters ### process startup parameters
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-h | -\? | --help) -h|-\?|--help)
# display help # display help
scriptHelp scriptHelp
;; ;;
-l|--log)
# set logfile location
if [ -z "$2" ]; then
consoleError '1' "Log file path cannot be null. Leave unspecified to save log in the same directory as this script."
fi
logfile="$2"
shift
;;
-v|--verbose)
verbose=1
;;
-d|--docker-compose)
# FULL path to docker-compose file
if [ -n "$2" ]; then
if [ -f "$2" ]; then
mcDockerCompose="$2"
shift
else
consoleError '1' "$1: cannot find docker-compose file as specified."
fi
else
consoleError '1' "$1: cannot be blank/empty."
fi
;;
-m|--mailcow-config)
# FULL path to mailcow configuration file file
if [ -n "$2" ]; then
if [ -f "$2" ]; then
mcConfig="$2"
shift
else
consoleError '1' "$1: cannot find mailcow configuration file as specified."
fi
else
consoleError '1' "$1: cannot be blank/empty."
fi
;;
-t1|--timeout-start)
if [ -z "$2" ]; then
consoleError '1' "$1: cannot be blank/empty."
else
dockerStartTimeout="$2"
shift
fi
;;
-t2|--timeout-stop)
if [ -z "$2" ]; then
consoleError '1' "$1: cannot be blank/empty."
else
dockerStopTimeout="$2"
shift
fi
;;
-b|--backup-location)
if [ -n "$2" ]; then
if [ -d "$2" ] && [ -n "$( ls -A "$2" )" ]; then
backupLocation="${2%/}"
shift
else
consoleError '1' "$1: cannot find specified backup location directory or it is empty."
fi
else
consoleError '1' "$1: cannot be blank/empty."
fi
;;
--skip-mail)
restoreMail=0
;;
--skip-sql)
restoreSQL=0
;;
--skip-postfix)
restorePostfix=0
;;
--skip-redis)
restoreRedis=0
;;
--skip-rspamd)
restoreRspamd=0
;;
*) *)
printf "\n%Unknown option: %s\n" "$err" "$1" printf "\n%sUnknown option: %s\n" "$err" "$1"
printf "Use '--help' for valid options.%s\n\n" "$norm" printf "Use '--help' for valid options.%s\n\n" "$norm"
exit 1 exit 1
;; ;;
@@ -160,18 +298,17 @@ while [ $# -gt 0 ]; do
done done
### pre-flight checks ### pre-flight checks
# ensure there's something to do
if [ "$restoreMail" -eq 0 ] && [ "$restoreSQL" -eq 0 ] && [ "$restorePostfix" -eq 0 ] && [ "$restoreRedis" -eq 0 ] && [ "$restoreRedis" -eq 0 ]; then
printf "\n%sAll restore operations skipped -- nothing to do!%s\n\n" "$yellow" "$norm"
exit 0
fi
# set path so checks are valid for this script environment
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# docker installed? # docker installed?
if ! command -v docker >/dev/null; then if ! command -v docker >/dev/null; then
consoleError '3' 'docker does not seem to be installed!' consoleError '3' 'docker does not seem to be installed!'
fi fi
# borg installed?
if ! command -v borg >/dev/null; then
consoleError '3' 'borgbackup does not seem to be installed!'
fi
# details file?
if [ ! -f "$configDetails" ]; then
consoleError '1' "configuration file ($configDetails) cannot be found."
fi
# mailcow.conf? # mailcow.conf?
if [ ! -f "$mcConfig" ]; then if [ ! -f "$mcConfig" ]; then
consoleError '1' "mailcow configuration file ($mcConfig) cannot be found." consoleError '1' "mailcow configuration file ($mcConfig) cannot be found."
@@ -180,11 +317,16 @@ fi
if [ ! -f "$mcDockerCompose" ]; then if [ ! -f "$mcDockerCompose" ]; then
consoleError '1' "docker-compose configuration ($mcDockerCompose) cannot be found." consoleError '1' "docker-compose configuration ($mcDockerCompose) cannot be found."
fi fi
# forgot to set backup location?
if [ -z "$backupLocation" ]; then
consoleError '1' "'--backup-location' cannot be unspecified or null/empty."
fi
# change to mailcow directory so commands execute properly
\cd "${mcConfig%/*}" || consoleError '4' 'Cannot change to mailcow directory as determined from mailcow.conf location.'
### read mailcow.conf and import vars ### read mailcow.conf and import vars
# shellcheck source=./mailcow.conf.shellcheck # shellcheck source=./mailcow.conf.shellcheck
. "$mcConfig" . "$mcConfig"
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout" export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout"
### start logging ### start logging
@@ -211,150 +353,294 @@ else
fi fi
fi fi
# write initial log entries # write initial log entries
if ! printf "%s[%s] --- Start %s execution ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" 2>/dev/null >>"$logfile"; then if ! printf "%s[%s] --- Start %s execution ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" 2>/dev/null >> "$logfile"; then
consoleError '1' "Unable to write to log file ($logfile)" consoleError '1' "Unable to write to log file ($logfile)"
fi fi
writeLog 'info' "Log located at $logfile" writeLog 'info' "Log located at $logfile"
### get location of docker volumes ### get location of docker volumes
dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1) dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1)
printf "%s[%s] -- [INFO] Using MAIL volume: %s --%s\n" \ writeLog 'info' "Using MAIL volume: ${dockerVolumeMail}"
"$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) dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1)
printf "%s[%s] -- [INFO] Using MAILCRYPT volume: %s --%s\n" \ writeLog 'info' "Using MAILCRYPT volume: ${dockerVolumeCrypt}"
"$cyan" "$(stamp)" "$dockerVolumeCrypt" "$norm" >>"$logfile" dockerVolumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_postfix-vol-1)
writeLog 'info' "Using POSTFIX volume: ${dockerVolumePostfix}"
dockerVolumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_redis-vol-1)
writeLog 'info' "Using REDIS volume: ${dockerVolumeRedis}"
dockerVolumeRspamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_rspamd-vol-1)
writeLog 'info' "Using RSPAMD volume: ${dockerVolumeRspamd}"
# exit if mail or crypt containers cannot be found (mailcow not initialized beforehand)
if [ -z "$dockerVolumeMail" ] || [ -z "$dockerVolumeCrypt" ]; then
writeLog 'error' '5' "Cannot find mail volume. Mailcow probably not initialized before running restore."
exitError 5
fi
### source configuration details file ### restore SQL
case "${configDetails}" in if [ "$restoreSQL" -eq 1 ]; then
/*) writeLog 'task' "Restoring mailcow database"
# absolute path, no need to rewrite variable
# shellcheck source=./backup.details
. "${configDetails}"
;;
*)
# relative path, prepend './' to create absolute path
# shellcheck source=./backup.details
. "./${configDetails}"
;;
esac
writeLog 'info' "Configuration file: ${yellow}${configDetails}${info} imported"
### verify borg variables # sql restore pre-requisites
# verify borg base directory sqlBackup=$(find "${backupLocation}/tmp" -iname "*.sql")
writeLog 'task' 'Verify details:borgBaseDir' if [ -n "$sqlBackup" ]; then
if [ -z "${borgBaseDir}" ]; then # start mysql container if not already running
badDetails empty 'borgBaseDir' if ! docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 > /dev/null 2>&1; then
elif [ ! -d "${borgBaseDir}" ]; then docker-compose up -d mysql-mailcow > /dev/null 2>&1
badDetails dne 'borgBaseDir' if docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 > /dev/null 2>&1; then
sqlRunning=1
else
writeLog 'done' 'error'
writeLog 'error' '12' "Cannot start mysql-mailcow container -- cannot restore mailcow database!"
errorCount=$((errorCount+1))
fi
else
sqlRunning=1
fi
else
writeLog 'done' 'error'
writeLog 'error' '11' "Cannot locate SQL backup -- cannot restore mailcow database!"
errorCount=$((errorCount+1))
fi
# restore sql
if [ "$sqlRunning" -eq 1 ]; then
if docker exec -i "$(docker-compose ps -q mysql-mailcow)" mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < "${sqlBackup}" > /dev/null 2>&1; then
writeLog 'done'
else
writeLog 'done' 'error'
writeLog 'error' '13' "Something went wrong while trying to restore SQL database. Perhaps try again?"
errorCount=$((errorCount+1))
fi
fi
fi fi
export BORG_BASE_DIR="${borgBaseDir%/}"
writeLog 'done' ### stop containers (necessary for all restore operations except SQL)
# check path to SSH keyfile writeLog 'task' "Stopping mailcow"
writeLog 'task' 'Verify details:borgSSHKey' if ! docker-compose down --timeout "${dockerStopTimeout}" > /dev/null 2>&1; then
if [ -z "${borgSSHKey}" ]; then writeLog 'done' 'error'
badDetails empty 'borgSSHKey' writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
elif [ ! -f "${borgSSHKey}" ]; then exitError 20
badDetails dne 'borgSSHKey'
fi fi
export BORG_RSH="ssh -i ${borgSSHKey}" if [ "$( docker ps --filter "name=${COMPOSE_PROJECT_NAME}" -q | wc -l )" -gt 0 ]; then
writeLog 'done' writeLog 'done' 'error'
# check borg repo connect string writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
writeLog 'task' 'Verify details:borgConnectRepo' exitError 20
if [ -z "${borgConnectRepo}" ]; then
badDetails empty 'borgConnectRepo'
fi fi
export BORG_REPO="${borgConnectRepo}"
writeLog 'done' writeLog 'done'
# check borg repo password
writeLog 'task' 'Verify details:borgRepoPassphrase' ### restore mail and encryption key
if [ -z "${borgRepoPassphrase}" ]; then if [ "$restoreMail" -eq 1 ]; then
# an empty repo passphrase is considered a mistake so throw an error if [ "$verbose" -eq 1 ]; then
# if the user meant to enter an empty passphrase they should use 'NONE' writeLog 'info' "Restoring email"
badDetails empty 'borgRepoPassphrase' else
elif [ "${borgRepoPassphrase}" = 'NONE' ]; then writeLog 'task' "Restoring email"
# password intentionally blank, use but issue warning fi
export BORG_PASSPHRASE=''
# restore email messages
doRestore "${COMPOSE_PROJECT_NAME}_vmail-vol-1" "$dockerVolumeMail"; ec="$?"
case "$ec" in
0)
if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Email messages restored"
else
writeLog 'done'
fi
;;
1)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more email messages."
else
writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more email messages."
fi
;;
2)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate email message backups!"
else
writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate email message backups!"
fi
;;
esac
# restore encryption key
doRestore "${COMPOSE_PROJECT_NAME}_crypt-vol-1" "$dockerVolumeCrypt"; ec="$?"
case "$ec" in
0)
if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Encryption key restored"
else
writeLog 'done'
fi
;;
1)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!"
else
writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!"
fi
;;
2)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate encryption key backup!"
else
writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate encryption key backup!"
fi
;;
esac
fi
### restore postfix
if [ "$restorePostfix" -eq 1 ]; then
if [ "$verbose" -eq 1 ]; then
writeLog 'info' "Restoring postfix files"
else
writeLog 'task' "Restoring postfix files"
fi
doRestore "${COMPOSE_PROJECT_NAME}_postfix-vol-1" "$dockerVolumePostfix"; ec="$?"
case "$ec" in
0)
if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Postfix files restored"
else
writeLog 'done'
fi
;;
1)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more postfix files."
else
writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more postfix files."
fi
;;
2)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate postfix backups!"
else
writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate postfix backups!"
fi
;;
esac
fi
### restore rspamd
if [ "$restoreRspamd" -eq 1 ]; then
if [ "$verbose" -eq 1 ]; then
writeLog 'info' "Restoring Rspamd files"
else
writeLog 'task' "Restoring Rspamd files"
fi
doRestore "${COMPOSE_PROJECT_NAME}_rspamd-vol-1" "$dockerVolumeRspamd"; ec="$?"
case "$ec" in
0)
if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Rspamd files restored"
else
writeLog 'done'
fi
;;
1)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more Rspamd files."
else
writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more Rspamd files."
fi
;;
2)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate Rspamd backups!"
else
writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate Rspamd backups!"
fi
;;
esac
fi
### restore redis
if [ "$restoreRedis" -eq 1 ]; then
if [ "$verbose" -eq 1 ]; then
writeLog 'info' "Restoring redis database"
else
writeLog 'task' "Restoring redis database"
fi
doRestore "${COMPOSE_PROJECT_NAME}_redis-vol-1" "$dockerVolumeRedis"; ec="$?"
case "$ec" in
0)
if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Redis database restored"
else
writeLog 'done'
fi
;;
1)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue."
else
writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue."
fi
;;
2)
if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate redis database backups!"
else
writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate redis database backups!"
fi
;;
esac
fi
### restart mailcow
writeLog 'task' "Starting mailcow"
if ! docker-compose up -d > /dev/null 2>&1; then
writeLog 'done' 'warn' writeLog 'done' 'warn'
writeLog 'warn' 'Using a borg repo with a blank password is an insecure configuration!' writeLog 'warn' '21' "Unable to automatically start mailcow containers. Please attempt a manual start and note any errors."
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
else
export BORG_PASSPHRASE="${borgRepoPassphrase}"
writeLog 'done'
fi fi
# check borg repo keyfile location writeLog 'done'
writeLog 'task' 'Verify details:borgKeyfileLocation'
if [ -z "${borgKeyfileLocation}" ]; then
# will use default location
writeLog 'done'
else
# verify keyfile location exists
if [ ! -f "${borgKeyfileLocation}" ]; then
badDetails dne 'bogKeyfileLocation'
fi
export BORG_KEY_FILE="${borgKeyfileLocation}"
writeLog 'done'
fi
# export borg remote path, if specified
if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; 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" 2>/dev/null; then
writeLog 'error' '31' "Unable to create borg temp directory (${borgBaseDir}/tmp)"
exitError 31
fi
fi
export TMPDIR="${borgBaseDir}/tmp"
### change to mailcow directory so docker commands run properly
cd "$(dirname ${mcConfig})" || writeLog 'error' '100' "Could not change to mailcow directory." && exitError 100
#TODO: stop containers
#TODO: pull backup via borg
#TODO: copy backups to correct docker volumes
#TODO: copy additional files to correct locations
#TODO: restart docker containers
#TODO: optionally reindex dovecot (parameter)
#TODO: delete downloaded backup (parameter)
### exit gracefully ### exit gracefully
writeLog 'success' "All processes completed"
printf "%s[%s] --- %s execution completed ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logfile"
# note non-terminating errors
if [ "$errorCount" -gt 0 ]; then if [ "$errorCount" -gt 0 ]; then
printf "%s%s errors encountered!%s\n" "$err" "$errorCount" "$norm" >>"$logfile" # note non-terminating errors
printf "%s[%s] --- %s execution completed with %s error(s) ---\n%s" "$err" "$(stamp)" "$scriptName" "$errorCount" "$norm" >> "$logfile"
exit 98
elif [ "$warnCount" -gt 0 ]; then
printf "%s[%s] --- %s execution completed with %s warning(s) ---\n%s" "$yellow" "$(stamp)" "$scriptName" "$warnCount" "$norm" >> "$logfile"
exit 97
else
writeLog 'success' "All processes completed"
printf "%s[%s] --- %s execution completed ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit 0
fi fi
# note warnings
if [ "$warnCount" -gt 0 ]; then
printf "%s%s warnings issued!%s\n" "$yellow" "$warnCount" "$norm" >>"$logfile"
fi
exit 0
### error codes: ### error codes:
# 1: parameter error # 1: parameter error
# 2: not run as root # 2: not run as root
# 3: borg or docker not installed # 3: docker not installed
# 10: null configuration variable in details file # 4: cannot change to mailcow directory
# 11: invalid configuration variable in details file # 5: mailcow not initialized before running script
# 1x: SQL errors
# 11: cannot locate SQL dump in backup directory
# 12: cannot start mysql-mailcow container
# 13: restoring SQL dump was unsuccessful
# 2x: Docker/Docker-Compose errors
# 20: cannot bring docker container(s) down successfully
# 21: cannot bring docker container(s) up successfully
# 5x: File restore errors
# 51: cannot locate source files in backup directory
# 52: error restoring one or more files
# 97: script completed with 1 or more warnings
# 98: script completed with 1 or more non-terminating errors
# 99: TERM signal trapped # 99: TERM signal trapped
# 100: could not change to mailcow-dockerized directory
# 101: could not stop container(s)
# 102: could not start container(s)
# 110: borg exited with a critical error
#EOF #EOF