Compare commits

...

65 Commits

Author SHA1 Message Date
Asif Bacchus 46b29dfce9 fix(script): fix dockerCmd quoting error 2022-07-15 13:49:59 -06:00
Asif Bacchus 47adaceed0 feature(version): add version display to script 2022-07-12 15:07:36 -06:00
Asif Bacchus 475a0d0dd2 refactor(compose): detect proper compose cmd 2022-07-12 14:55:43 -06:00
Asif Bacchus 818483f92e refactor(vars): quote dockerCmd calls 2022-07-12 14:54:17 -06:00
Asif Bacchus c8b328e707 fix(typos): fix various typos 2022-07-12 12:52:37 -06:00
Asif Bacchus fb4dcab29a chore(shellcheck): ignore some warnings 2022-07-12 12:50:42 -06:00
Asif Bacchus 5382dd26d9 fix(cleanup): prevent doDocker error loop
- if doDocker throws an error, cleanup is called which again calls doDocker
- resolve this loop by passing a variable from cleanup
2022-07-12 12:47:52 -06:00
Asif Bacchus bd36035a9f refactor(compose): update container name lookup
- filter by both compose v1 and v2 naming conventions
2022-07-12 12:34:26 -06:00
Asif Bacchus 389f5ef059 feature(compose): detect and use proper compose cmd
- check for compose v2 and use 'docker compose'
- if v2 not detected, fallback to 'docker-compose'
2022-07-12 12:31:24 -06:00
Asif Bacchus 8d10a1a3b9 feature(version): add param to display version
- as per user request
2022-07-12 11:36:57 -06:00
Asif Bacchus 1ac614ee60 refactor(license): update license
- update dates
- add contact email address
2022-07-12 11:36:28 -06:00
Asif Bacchus d0141e3296 chore(ide): additional Rider config files 2022-07-12 11:35:58 -06:00
Asif Bacchus 17bd522ff7 chore(git): update git control files 2022-07-12 11:35:20 -06:00
Asif Bacchus 5aa9068556 chore(git): update IDE config tracking
- remove VSCode
- add selected Rider files
2022-07-12 11:35:01 -06:00
Asif Bacchus aa2c17799c fix(DETAILS): remove erroneous spaces
Closes github #11
2021-03-08 03:45:18 -07:00
Asif Bacchus 1334975cab fix(BACKUP): fix colour var usage
SQL dump success and REDIS dump success msg printed 'cyan' instead of
using the colour var
2021-03-08 00:50:55 -07:00
Asif Bacchus 19d1eb1587 refactor(BACKUP): prevent alias cmds
- prefix cp and cd with \ to prevent alias cmd
- fix typo in comment
2021-03-08 00:40:11 -07:00
Asif Bacchus 4aae61f97e feature(BACKUP): allow non-default borg location
- parameter to specify non-default borg program location

Closes github #10
2021-03-08 00:36:32 -07:00
Asif Bacchus 5ea25529b9 feature(BACKUP): add SSH port customization
- borgSSHPort variable in details file
- read and appended to RSH connection string
- default to standard port 22

Closes github #9
2021-03-08 00:08:22 -07:00
Asif Bacchus de76918e81 feature(BACKUP): parameterize log colourization
Closes github #9
2021-03-07 23:54:32 -07:00
Asif Bacchus 16f4c11f91 fix(BACKUP): remove extraneous mailcow dir in xtraLocation 2021-02-15 15:43:18 -07:00
Asif Bacchus 0bc0f8e5f9 refactor(BACKUP): update xtraLocation sample 2021-02-15 15:39:43 -07:00
Asif Bacchus 21e4a6774c docs(BACKUP): change xtraListPath description
- description was misleading, this is not a required variable
- holdover from importing
2021-02-15 15:37:32 -07:00
Asif Bacchus 7e739d96a1 feature(LOGWATCH): Update logwatch scripts
- add trailing newlines
- update search phrases to match logfile output
2021-02-15 15:31:05 -07:00
Asif Bacchus 61e505952d update TOC 2021-02-10 08:30:13 -07:00
Asif Bacchus 1939962b75 [README] add restore section 2021-02-10 08:08:18 -07:00
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
Asif Bacchus 5c7064e2bd style(RESTORE): reformat 2021-02-05 03:11:10 -07:00
Asif Bacchus 1cbbf3d66d feature(RESTORE): restore script skeleton
- non functional, scaffolding only
2021-02-05 03:09:46 -07:00
Asif Bacchus 2b5f8a0887 fix(BACKUP): initialize sqlDumpDirCreated
- initialize sqlDumpDirCreated to avoid 'unary operator expected' error

Closes 7
2021-02-05 01:11:16 -07:00
Asif Bacchus 85fae7f6c8 build(SHELLCHECK): add source paths
- add dummy mailcow.conf
- add source directives
- allows easier error checking using shellcheck
2021-02-05 01:08:23 -07:00
Asif Bacchus 32daba03b3 build(RIDER): add rider environment files 2021-02-05 01:07:12 -07:00
18 changed files with 1197 additions and 284 deletions

48
.gitattributes vendored
View File

@ -10,17 +10,17 @@
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
@ -30,6 +30,7 @@
*.tsv text
*.txt text
*.sql text
*.ps1 text eol=crlf
# Graphics
*.png binary
@ -48,11 +49,28 @@
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# web frontend stack -- force LF so SRI hashes are always correct
*.html text eol=lf
*.htm text eol=lf
*.css text eol=lf
*.min.css text eol=lf
*.js text eol=lf
*.min.js text eol=lf
*.php text eol=lf
# Visual Studio projects (Rider also)
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Serialisation
*.json text
@ -65,12 +83,18 @@
*.7z binary
*.gz binary
*.tar binary
*.tgz binary
*.zip binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
.idea export-ignore
.vscode export-ignore

35
.gitignore vendored
View File

@ -1,10 +1,27 @@
# ignore all vscode config files
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/numbered-bookmarks.json
#
# JetBrains exclusions
#
# ignore all generated logfiles
*.log
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# modules
.idea_modules/
# Editor-based Rest Client
.idea/httpRequests

View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.MailcowBackup.iml
/projectSettingsUpdater.xml
/modules.xml
/contentModel.xml
# Datasource local storage ignored files
/../../../../../../../../../:\Users\AsifB\Documents\RiderProjects\MailcowBackup\.idea\.idea.MailcowBackup.dir\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="BodyLimit" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SubjectBodySeparation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="RIGHT_MARGIN" value="50" />
</inspection_tool>
</profile>
</component>
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list>
<option value="master" />
<option value="main" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,3 +0,0 @@
{
"bookmarks": []
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Asif Bacchus
Copyright (c) 2020-2022 Asif Bacchus (asif@asifbacchus.dev)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,4 +1,4 @@
# Mailcow Backup Using borgbackup <!-- omit in toc -->
# Mailcow Backup Using borgbackup
This script automates backing up your Mailcow installation using borgbackup and a remote ssh-capable storage system. I suggest using rsync.net since they
have great speeds and a special pricing structure for borgbackup/attic users ([details here](https://www.rsync.net/products/attic.html)).
@ -14,14 +14,19 @@ This script automates the following tasks:
- Runs 'borg prune' to make sure you are trimming old backups on your schedule
- Creates a clear, easy to parse log file so you can keep an eye on your backups and any errors/warnings
## Contents <!-- omit in toc -->
## contents
<!-- toc -->
- [quick start](#quick-start)
- [configuration file](#configuration-file)
- [running the script](#running-the-script)
- [scheduling your backup via cron](#scheduling-your-backup-via-cron)
- [restoring backups](#restoring-backups)
- [final notes](#final-notes)
<!-- tocstop -->
## quick start
Clone this repo or download a release file into a directory of your choosing. For all examples in this document, I will assume you will run the script from */scripts/backup*. Make sure the script file is executable and you protect the *.details* file since it contains things like your repo password:
@ -111,6 +116,10 @@ Edit your root user's crontab and add an entry like this which would run the scr
7 1 * * * /scripts/backup/backup.sh -l /var/log/mailcow_backup.log > /dev/null 2>&1
```
## restoring backups
Starting with version 3.0, a *restore.sh* file has been included to semi-automate restoring your backups to a clean mailcow instance. There are a few steps required and they are better explained in the wiki than would be possible in a short write-up like this. Please check out the [restore process overview](https://git.asifbacchus.app/asif/MailcowBackup/wiki/8.0-Restore-overview) for more information.
## final notes
I think that's everything. For detailed information, please review the [wiki](https://git.asifbacchus.app/asif/MailcowBackup/wiki/_pages). If I've forgotten to document something there, please let me know. I know the wiki is long but, I hate how much stuff for Linux and open-source programs/scripts in general are so poorly documented especially for newbies and I didn't want to make that same mistake.

View File

@ -20,6 +20,11 @@
# default is "$HOME" or "~$USER" in that order. If you're unsure, try "$HOME"
borgBaseDir="/var/borgbackup"
# SSH port on which your borg server listens. To connect on a custom port,
# uncomment the line below and change the port number as required.
# If you leave this line commented, the default (port 22) will be used.
#borgSSHPort=22
# full path to the SSH key used to connect to your remote backup server
borgSSHKey="/var/borgbackup/private.key"
@ -36,12 +41,13 @@ borgRepoPassphrase="p@ssW0rd"
# 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"
#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!
# additional files to backup
# by default, the script will only backup your mailcow program directory
# (e.g. /opt/mailcow-dockerized) and your mailcow data volumes. If you would
# like additional files included (perhaps certificates, system configuration,
# etc.) include those locations listed one item per line in the file below
# see repo wiki for more details
borgXtraListPath="/scripts/backup/xtraLocations.borg"
@ -58,4 +64,4 @@ borgPruneSettings="--keep-within=14d --keep-daily=30 --keep-weekly=12 --keep-mon
# location of borg instance on your remote backup server
# this is very often just "borg1"
borgRemote="borg1"
borgRemote="borg1"

View File

@ -8,9 +8,8 @@
### 3. remote repo already set-up and configured
#######
### text formatting presents
if command -v tput > /dev/null; then
if command -v tput >/dev/null; then
bold=$(tput bold)
cyan=$(tput setaf 6)
err=$(tput bold)$(tput setaf 1)
@ -32,11 +31,9 @@ else
yellow=""
fi
### trap
trap trapExit 1 2 3 6
### functions
# bad configuration value passed in details file
@ -73,73 +70,74 @@ badParam() {
cleanup() {
# cleanup 503 if copied
if [ "$err503Copied" -eq 1 ]; then
# shellcheck disable=SC2129
if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then
printf "%s[%s] -- [WARNING] Could not remove 503 error page." \
"$warn" "$(stamp)" >> "$logFile"
"$warn" "$(stamp)" >>"$logFile"
printf " Web interface will not function until this file is " \
>> "$logFile"
printf "removed --%s\n" "$norm" >> "$logFile"
warnCount=$((warnCount+1))
>>"$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
"$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"
"$warn" "$(stamp)" "$norm" >>"$logFile"
else
printf "%s[%s] -- [INFO] Temporary SQL-dump directory removed successfully --%s\n" \
"$cyan" "$(stamp)" "$norm" >> "$logFile"
"$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"
"$cyan" "$(stamp)" "$norm" >>"$logFile"
else
exitError 102 'Could not start POSTFIX container.'
exitError 102 'Could not start POSTFIX container.' 'final'
fi
doDocker start dovecot
if [ "$dockerResultState" = "true" ]; then
printf "%s[%s] -- [INFO] DOVECOT container is running --%s\n" \
"$cyan" "$(stamp)" "$norm" >> "$logFile"
"$cyan" "$(stamp)" "$norm" >>"$logFile"
else
exitError 102 'Could not start DOVECOT container.'
exitError 102 'Could not start DOVECOT container.' 'final'
fi
}
doDocker() {
containerName="$( docker ps -a --format '{{ .Names }}' --filter name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1 )"
containerName="$(docker ps -a --format '{{ .Names }}' --filter "name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1" --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"
"$cyan" "$(stamp)" "$2" "$norm" >>"$logFile"
${dockerCmd} -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 )"
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"
"$cyan" "$(stamp)" "$2" "$norm" >>"$logFile"
${dockerCmd} -f "$mcDockerCompose" start "$2-mailcow" 2>>"$logFile"
# set result vars
dockerResultState="$( docker inspect -f '{{ .State.Running }}' $containerName )"
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
"$err" "$(stamp)" "$1" "$2" "$norm" >>"$logFile"
if [ ! "$3" = "final" ]; then cleanup; fi
# note script completion with error
printf "%s[%s] --- %s execution completed with error ---%s\n" \
"$err" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
"$err" "$(stamp)" "$scriptName" "$norm" >>"$logFile"
exit "$1"
}
@ -147,12 +145,17 @@ exitError() {
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."
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 "-b | --borg"
textblock "FULL path to borg executable file if not in the default location."
defaultsTextblock "(/usr/bin/borg)"
newline
switchTextblock "-c | --config | --details"
textblock "Path to the configuration key/value-pair file for this script."
defaultsTextblock "(scriptPath/scriptName.details)"
@ -164,6 +167,14 @@ scriptHelp() {
textblock "Path to write log file"
defaultsTextblock "(scriptPath/scriptName.log)"
newline
switchTextblock "[SWITCH] --nc | --no-color | --no-colour"
textblock "Do NOT use ANSI colourization in the log file (in case your preferred log viewer does not support it)."
defaultsTextblock "(use ANSI colourization to make log file look good)"
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"
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)"
@ -185,7 +196,7 @@ scriptHelp() {
textblock "${magenta}--- mailcow related ---${norm}"
newline
switchTextblock "-d | --docker-compose"
textblock "Path to mailcow's 'docker-compose.yml' file."
textblock "Path to the mailcow 'docker-compose.yml' file."
defaultsTextblock "(/opt/mailcow-dockerized/docker-compose.yml)"
newline
switchTextblock "-m | --mailcow-config"
@ -229,29 +240,33 @@ newline() {
# same as exitError but for signal captures
trapExit() {
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" \
"$err" "$(stamp)" "$norm" >> "$logFile"
"$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"
"$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" )"
scriptVersion="4.0"
scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)"
scriptName="$(basename "$0")"
logFile="$scriptPath/${scriptName%.*}.log"
colourizeLogFile=1
warnCount=0
configDetails="$scriptPath/${scriptName%.*}.details"
err503Copied=0
sqlDumpDirCreated=0
exclusions=0
dockerCmd="docker compose"
borgPath="/usr/bin/borg"
# borg output verbosity -- normal
borgCreateParams='--stats'
borgPruneParams='--list'
@ -268,137 +283,172 @@ 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%/}"
-h | -\? | --help)
# display help
scriptHelp
exit 0
;;
--version)
# display script version
printf "\nMailcowBackup - Backup Mailcow using borgbackup to a remote SSH server : Version %s\n\n" ${scriptVersion}
exit 0
;;
-l | --log)
# set log file location
if [ -n "$2" ]; then
logFile="${2%/}"
shift
else
badParam empty "$@"
fi
;;
--nc | --no-color | --no-colour)
# do NOT colourize log file
colourizeLogFile=0
;;
-b | --borg)
# specify non-default borg path
if [ -n "$2" ]; then
borgPath="${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 empty "$@"
badParam dne "$@"
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
;;
--compression)
# set borg archive compression
if [ -n "$2" ]; then
borgCompression="$2"
shift
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 empty "$@"
badParam dne "$@"
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 empty "$@"
badParam dne "$@"
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 empty "$@"
badParam dne "$@"
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 empty "$@"
badParam dne "$@"
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
;;
else
badParam empty "$@"
fi
;;
-t1 | --timeout-start)
if [ -z "$2" ]; then
badParam empty "$@"
else
dockerStartTimeout="$2"
shift
fi
;;
-t2 | --timeout-stop)
if [ -z "$2" ]; then
badParam empty "$@"
else
dockerStopTimeout="$2"
shift
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
if [ "$(id -u)" -ne 0 ]; then
printf "\n%sERROR: script MUST be run as ROOT%s\n\n" "$err" "$norm"
exit 2
fi
# verify compose installed and set proper commands based on version
if ! (docker compose version >/dev/null 2>&1); then
if ! (docker-compose --version >/dev/null 2>&1); then
printf "\n%sERROR: Docker Compose not installed or not functioning%s\n\n" "$err" "$norm"
exit 3
fi
dockerCmd="docker-compose"
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
if ! find "$borgPath" -type f -executable >/dev/null 2>&1; then
printf "\n%sERROR: BORG cannot be found in the specified or default location on this system!%s\n\n" "$err" "$norm"
exit 4
fi
# if 503 functionality is enabled, do 503 related files exist?
if [ "$use503" -eq 1 ]; then
@ -416,58 +466,71 @@ fi
if [ ! -f "$mcDockerCompose" ]; then
badParam dne "(--docker-compose)" "$mcDockerCompose"
fi
# set compression level if specified
if [ -n "$borgCompression" ]; then
borgCreateParams="${borgCreateParams} --compression ${borgCompression}"
fi
# remove colourization if parameter specified
if [ "$colourizeLogFile" -eq 0 ]; then
bold=""
cyan=""
err=""
magenta=""
norm=""
ok=""
warn=""
yellow=""
fi
### read mailcow.conf and set vars as needed
# shellcheck source=./mailcow.conf.shellcheck
. "$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"
"$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logFile"
printf "%s[%s] -- [INFO] Log located at %s%s%s --%s\n" \
"$cyan" "$(stamp)" "$yellow" "$logFile" "$cyan" "$norm" >> "$logFile"
"$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"
"$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"
"$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"
"$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"
"$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"
"$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}"
;;
/*)
# 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
printf "%s[%s] -- [INFO] %s%s%s imported --%s\n" \
"$cyan" "$(stamp)" "$yellow" "$configDetails" "$cyan" "$norm" >> "$logFile"
"$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"
"$cyan" "$(stamp)" "$norm" >>"$logFile"
## verify borg base directory
if [ -z "${borgBaseDir}" ]; then
@ -476,7 +539,7 @@ elif [ ! -d "${borgBaseDir}" ]; then
badDetails dne 'borgBaseDir'
fi
printf "%sdetails:borgBaseDir %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
export BORG_BASE_DIR="${borgBaseDir%/}"
## check path to SSH keyfile
@ -486,28 +549,37 @@ 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}"
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
## check SSH port
if [ -z "${borgSSHPort}" ]; then
borgSSHPort=22
printf "%sdetails:borgSSHPort %s-- %s[DEFAULT]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
else
printf "%sdetails:borgSSHPort %s-- %s[CUSTOM]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
fi
export BORG_RSH="ssh -i ${borgSSHKey} -p ${borgSSHPort}"
## 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"
"$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"
"$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))
"$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"
@ -515,26 +587,26 @@ fi
## check borg repository keyfile location
if [ -z "${borgKeyfileLocation}" ]; then
printf "%sdetails:borgKeyfileLocation %s-- %s[DEFAULT]%s\n" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
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"
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
## check if exclusion list file is specified
if [ -n "${borgExcludeListPath}" ]; then
# check if the file actually exists
if [ ! -f "${borgExcludeListPath}" ]; then
badDetails dne 'borgExcludeListPath'
fi
exclusions=1
exclusions=1
fi
## read additional files
@ -553,25 +625,23 @@ else
xtraList="${xtraList} ${xtraItem}"
fi
done <<EOF
$( sed -e '/^\s*#.*$/d' -e '/^\s*$/d' "${borgXtraListPath}" )
$(sed -e '/^\s*#.*$/d' -e '/^\s*$/d' "${borgXtraListPath}")
EOF
printf "%sdetails:borgXtraListPath %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >> "$logFile"
printf "%sdetails:borgXtraListPath %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
fi
### set location of sql dump
# this is done before resetting default TMP dir for borg
if ! sqlDumpDir=$( mktemp -d 2>/dev/null ); then
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"
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"
"$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
@ -584,66 +654,62 @@ if [ ! -d "${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"
"$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
"$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))
"$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"
"$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.'
\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"
"$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"
"$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 "$?" )
"$cyan" "$(stamp)" "$norm" >>"$logFile"
${dockerCmd} exec -T mysql-mailcow mysqldump --default-character-set=utf8mb4 \
-u${DBUSER} -p${DBPASS} ${DBNAME} >"$sqlDumpDir/$sqlDumpFile" 2>>"$logFile"
dumpResult=$(${dockerCmd} exec -T mysql-mailcow echo "$?")
if [ "$dumpResult" -eq 0 ]; then
printf "%s[%s] -- [INFO] SQL database dumped successfully --%s\n" \
"cyan" "$(stamp)" "$norm" >> "$logFile"
"$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
@ -651,24 +717,23 @@ if [ -f "$dockerVolumeRedis/dump.rdb" ]; then
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 "$?" )
"$cyan" "$(stamp)" "$norm" >>"$logFile"
${dockerCmd} exec -T redis-mailcow redis-cli save >>"$logFile" 2>&1
rdumpResult=$(${dockerCmd} exec -T redis-mailcow echo "$?")
if [ "$rdumpResult" -eq 0 ]; then
printf "%s[%s] -- [INFO] mailcow redis dumped successfully --%s\n" \
"cyan" "$(stamp)" "$norm" >> "$logFile"
"$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"
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} \
borgCMD="${borgPath} create --show-rc ${borgCreateParams} \
::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \
${sqlDumpDir} \
@ -679,7 +744,7 @@ if [ "$exclusions" -eq 0 ]; then
${dockerVolumeCrypt} \
${xtraList}"
elif [ "$exclusions" -eq 1 ]; then
borgCMD="borg --show-rc create ${borgCreateParams} \
borgCMD="${borgPath} create --show-rc ${borgCreateParams} \
--exclude-from ${borgExcludeListPath} \
::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \
@ -694,96 +759,95 @@ fi
# execute borg
printf "%s[%s] -- [INFO] Executing borg backup operation --%s\n" \
"$cyan" "$(stamp)" "$norm" >> "$logFile"
${borgCMD} 2>> "$logFile"
"$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"
"$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))
"$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))
"$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
### execute borg prune if parameters 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"
"$cyan" "$(stamp)" "$norm" >>"$logFile"
# shellcheck disable=SC2086
"${borgPath}" prune --show-rc -v ${borgPruneParams} ${borgPruneSettings} \
2>>"$logFile"
borgPruneResult="$?"
else
printf "%s[%s] -- [WARNING] No prune parameters provided. " \
"$warn" "$(stamp)" >> "$logFile"
"$warn" "$(stamp)" >>"$logFile"
printf "Your archive will continue growing with each backup --%s\n" \
"$norm" >> "$logFile"
warnCount=$((warnCount+1))
"$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"
"$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))
"$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"
"$warn" "$(stamp)" >>"$logFile"
printf "return code. Review this logfile for details --%s\n" \
"$norm" >> "$logFile"
warnCount=$((warnCount+1))
"$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"
"$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"
"$ok" "$(stamp)" "$norm" >>"$logFile"
printf "%s[%s] --- %s execution completed ---%s\n" \
"$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
"$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logFile"
if [ "$warnCount" -gt 0 ]; then
printf "%s%s warnings issued!%s\n" "$warn" "${warnCount}" "$norm" >> "$logFile"
printf "%s%s warnings issued!%s\n" "$warn" "${warnCount}" "$norm" >>"$logFile"
else
printf "%s0 warnings issued.%s\n" "$ok" "$norm" >> "$logFile"
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
# 3: docker compose not installed or not functioning
# 4: borg not installed
# 99: TERM signal trapped
# 100: could not change to mailcow-dockerized directory
# 101: could not stop container(s)
@ -794,4 +858,4 @@ exit 0
# 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
# 139: borg prune exited with a critical error

View File

@ -0,0 +1,66 @@
#
# sample mailcow configuration file
# (used by shellcheck)
#
MAILCOW_HOSTNAME=mail.example.com
DBNAME=mailcow
DBUSER=mailcow
DBPASS=verylongpassword
DBROOT=anotherlongpassword
HTTP_PORT=8080
HTTP_BIND=127.0.0.1
HTTPS_PORT=8443
HTTPS_BIND=127.0.0.1
SMTP_PORT=25
SMTPS_PORT=465
SUBMISSION_PORT=587
IMAP_PORT=143
IMAPS_PORT=993
POP_PORT=110
POPS_PORT=995
SIEVE_PORT=4190
DOVEADM_PORT=127.0.0.1:19991
SQL_PORT=127.0.0.1:13306
SOLR_PORT=127.0.0.1:18983
REDIS_PORT=127.0.0.1:7654
TZ=America/Edmonton
COMPOSE_PROJECT_NAME=mailcowdockerized
ACL_ANYONE=disallow
MAILDIR_GC_TIME=1440
ADDITIONAL_SAN=
SKIP_LETS_ENCRYPT=y
ENABLE_SSL_SNI=n
SKIP_IP_CHECK=n
SKIP_HTTP_VERIFICATION=n
SKIP_CLAMD=n
SKIP_SOGO=n
SKIP_SOLR=n
SOLR_HEAP=1024
USE_WATCHDOG=y
ALLOW_ADMIN_EMAIL_LOGIN=n
WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com
WATCHDOG_NOTIFY_BAN=y
WATCHDOG_EXTERNAL_CHECKS=y
LOG_LINES=9999
IPV4_NETWORK=172.22.1
IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
#SNAT_TO_SOURCE=
#SNAT6_TO_SOURCE=
#API_KEY=
#API_KEY_READ_ONLY=
#API_ALLOW_FROM=172.22.1.1,127.0.0.1
MAILDIR_SUB=Maildir
SOGO_EXPIRE_SESSION=480
DOVECOT_MASTER_USER=
DOVECOT_MASTER_PASS=
MAILCOW_PASS_SCHEME=BLF-CRYPT

666
backup/restore.sh Executable file
View File

@ -0,0 +1,666 @@
#!/bin/sh
#######
### restore mailcow from borgbackup repository
### this assumes three things:
### 1. standard mailcow-dockerized setup as per the docs
### 2. backups made using the backup script from this git repo
### 3. backups already downloaded from your borg repo
#######
### text-formatting presets
if command -v tput >/dev/null; then
bold=$(tput bold)
cyan=$(tput bold)$(tput setaf 6)
err=$(tput bold)$(tput setaf 1)
info=$(tput sgr0)
magenta=$(tput sgr0)$(tput setaf 5)
norm=$(tput sgr0)
ok=$(tput setaf 2)
width=$(tput cols)
yellow=$(tput sgr0)$(tput setaf 3)
else
bold=''
cyan=''
err=''
info=''
magenta=''
norm=''
ok=''
width=80
yellow=''
fi
### trap
trap trapExit 1 2 3 6
### functions
consoleError() {
printf "\n%s%s\n" "$err" "$2"
printf "Exiting.\n\n%s" "$norm"
exit "$1"
}
exitError() {
printf "%s[%s] --- %s execution completed with error ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile"
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() {
textNewline
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
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
exit 0
}
stamp() {
(date +%F' '%T)
}
textblock() {
printf "%s\n" "$1" | fold -w "$width" -s
}
textblockHeader() {
printf "\n%s%s*** %s ***%s\n\n" "$bold" "$magenta" "$1" "$norm"
}
textblockParam() {
if [ -z "$2" ]; then
# no default
printf "%s%s%s\n" "$cyan" "$1" "$norm"
else
# default parameter provided
printf "%s%s %s(%s)%s\n" "$cyan" "$1" "$yellow" "$2" "$norm"
fi
}
textNewline() {
printf "\n"
}
trapExit() {
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" "$err" "$(stamp)" "$norm" >>"$logfile"
printf "%s[%s] --- %s execution terminated via signal ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile"
exit 99
}
writeLog() {
if [ "$1" = "task" ]; then
printf "%s[%s] -- [INFO] %s... " "$info" "$(stamp)" "$2" >>"$logfile"
elif [ "$1" = "done" ]; then
if [ -z "$2" ]; then
printf "%sdone%s --\n%s" "$ok" "$info" "$norm" >>"$logfile"
elif [ "$2" = "error" ]; then
printf "%sERROR%s --\n%s" "$err" "$info" "$norm" >>"$logfile"
elif [ "$2" = "warn" ]; then
printf "%swarning%s --\n%s" "$yellow" "$info" "$norm" >>"$logfile"
fi
elif [ "$1" = "error" ]; then
printf "%s[%s] -- [ERROR] %s: %s --\n%s" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logfile"
elif [ "$1" = "warn" ]; then
printf "%s[%s] -- [WARNING] %s --\n%s" "$yellow" "$(stamp)" "$2" "$norm" >>"$logfile"
elif [ "$1" = "info" ]; then
printf "%s[%s] -- [INFO] %s --\n%s" "$info" "$(stamp)" "$2" "$norm" >>"$logfile"
elif [ "$1" = "success" ]; then
printf "%s[%s] -- [SUCCESS] %s --\n%s" "$ok" "$(stamp)" "$2" "$norm" >>"$logfile"
fi
}
### parameter defaults
# script related
scriptVersion="4.0"
scriptPath="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
scriptName="$(basename "$0")"
errorCount=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="$scriptPath/${scriptName%.*}.log"
# mailcow/docker related
mcConfig='/opt/mailcow-dockerized/mailcow.conf'
mcDockerCompose='/opt/mailcow-dockerized/docker-compose.yml'
sqlRunning=0
dockerStartTimeout=180
dockerStopTimeout=120
dockerCmd="docker compose"
### check if user is root
if [ "$(id -u)" -ne 0 ]; then
consoleError '2' "This script must be run as ROOT."
fi
### process startup parameters
while [ $# -gt 0 ]; do
case "$1" in
-h | -\? | --help)
# display help
scriptHelp
;;
--version)
# display script version
printf "\nMailcowRestore - Restore Mailcow from a borgbackup repo : Version %s\n\n" ${scriptVersion}
exit 0
;;
-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%sUnknown option: %s\n" "$err" "$1"
printf "Use '--help' for valid options.%s\n\n" "$norm"
exit 1
;;
esac
shift
done
### 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?
if ! command -v docker >/dev/null; then
consoleError '3' 'docker does not seem to be installed!'
fi
# verify compose installed and set proper commands based on version
if ! (docker compose version >/dev/null 2>&1); then
if ! (docker-compose --version >/dev/null 2>&1); then
printf "\n%sERROR: Docker Compose not installed or not functioning%s\n\n" "$err" "$norm"
exit 3
fi
dockerCmd="docker-compose"
fi
# mailcow.conf?
if [ ! -f "$mcConfig" ]; then
consoleError '1' "mailcow configuration file ($mcConfig) cannot be found."
fi
# docker-compose configuration?
if [ ! -f "$mcDockerCompose" ]; then
consoleError '1' "docker-compose configuration ($mcDockerCompose) cannot be found."
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
# shellcheck source=./mailcow.conf.shellcheck
. "$mcConfig"
export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout"
### start logging
# verify logfile specification is valid
if ! printf "%s" "$logfile" | grep -o / >/dev/null; then
# no slashes -> filename provided, save in scriptdir
logfile="$scriptPath/$logfile"
elif [ "$(printf "%s" "$logfile" | tail -c 1)" = '/' ]; then
# ends in '/' --> directory provided, does it exist?
if [ ! -d "$logfile" ]; then
if ! mkdir -p "$logfile" >/dev/null 2>&1; then
consoleError '1' "Unable to make specified log file directory."
fi
fi
logdir="$(cd "$logfile" 2>/dev/null && pwd -P)"
logfile="${logdir}/${scriptName%.*}.log"
else
# full path provided, does the parent directory exist?
if [ ! -d "${logfile%/*}" ]; then
# make parent path
if ! mkdir -p "${logfile%/*}" >/dev/null 2>&1; then
consoleError '1' "Unable to make specified log file path."
fi
fi
fi
# write initial log entries
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)"
fi
writeLog 'info' "Log located at $logfile"
### get location of docker volumes
dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1)
writeLog 'info' "Using MAIL volume: ${dockerVolumeMail}"
dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1)
writeLog 'info' "Using MAILCRYPT volume: ${dockerVolumeCrypt}"
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
### restore SQL
if [ "$restoreSQL" -eq 1 ]; then
writeLog 'task' "Restoring mailcow database"
# sql restore pre-requisites
sqlBackup=$(find "${backupLocation}/tmp" -iname "*.sql")
if [ -n "$sqlBackup" ]; then
# start mysql container if not already running
if ! docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 >/dev/null 2>&1; then
${dockerCmd} up -d mysql-mailcow >/dev/null 2>&1
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 "$(${dockerCmd} 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
### stop containers (necessary for all restore operations except SQL)
writeLog 'task' "Stopping mailcow"
if ! ${dockerCmd} down --timeout "${dockerStopTimeout}" >/dev/null 2>&1; then
writeLog 'done' 'error'
writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
exitError 20
fi
if [ "$(docker ps --filter "name=${COMPOSE_PROJECT_NAME}" -q | wc -l)" -gt 0 ]; then
writeLog 'done' 'error'
writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
exitError 20
fi
writeLog 'done'
### restore mail and encryption key
if [ "$restoreMail" -eq 1 ]; then
if [ "$verbose" -eq 1 ]; then
writeLog 'info' "Restoring email"
else
writeLog 'task' "Restoring email"
fi
# 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 ! ${dockerCmd} up -d >/dev/null 2>&1; then
writeLog 'done' 'warn'
writeLog 'warn' '21' "Unable to automatically start mailcow containers. Please attempt a manual start and note any errors."
warnCount=$((warnCount + 1))
fi
writeLog 'done'
### exit gracefully
if [ "$errorCount" -gt 0 ]; then
# 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
### error codes:
# 1: parameter error
# 2: not run as root
# 3: docker not installed
# 4: cannot change to mailcow directory
# 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
#EOF

View File

@ -49,12 +49,12 @@
# include the paths to important configuration files/directories and/or
# data directories
# mailcow configuration (example)
/opt/mailcow-dockerized/
# mailcow configuration
# already backed-up by the script by default based on mailcow.conf location
# NGINX (example)
# NGINX (if this host is a reverse proxy, for example)
/etc/nginx/
/usr/share/nginx/html/
# LetsEncrypt (example)
# LetsEncrypt
/etc/letsencrypt/

View File

@ -11,4 +11,4 @@ Archive = /path/to/your/logfile.ext.?.gz
# You don't need to change this unless you have altered the 'stamp' variable in
# the script in which case you will want to update the regex in the custom pl
# script below
*sqFullStampAnywhere
*sqFullStampAnywhere

View File

@ -6,4 +6,4 @@ Title = "System and Mailcow Backup"
# Override the detail level for this service
# Remember the levels are: 0, 1-4, 5, 6+
# Detail = 0
# Detail = 0

View File

@ -3,8 +3,8 @@
#############################################################################
# $Id$
#############################################################################
# Log: Backup script (backup)
# Revision 1.0 2018/10/16
# Log: mailcow backup
# Revision 1.1 2019/07/20
# Written by Asif Bacchus
#############################################################################
@ -34,7 +34,7 @@ if ($detailLevel == 0) {
elsif ($ThisLine =~ /\-- \[WARNING\] /) {
$summaryWarn++;
}
elsif ($ThisLine =~ /All processes completed successfully/) {
elsif ($ThisLine =~ /All processes completed/) {
$summarySuccess++;
}
}
@ -109,4 +109,4 @@ exit (0);
# mode: perl
# perl-indent-level: 3
# indent-tabs-mode: nil
# End:
# End: