Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving backup_and_restore.sh script before new-coming features #6030

Open
wants to merge 31 commits into
base: staging
Choose a base branch
from
Open
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3311ae
Adding `print_usage` function to `backup_and_restore.sh` script
h3ssan Aug 19, 2024
2460a98
Add colored messages in `backup_and_restore.sh` script
h3ssan Aug 19, 2024
5eceecf
Move the `thread count notice` into the usage menu
h3ssan Aug 19, 2024
82def75
Fix: Double quoted variable `BACKUP_LOCATION`.
h3ssan Aug 19, 2024
a621aea
Bug Fix: overwriting filesystem possible
h3ssan Aug 19, 2024
0d95962
Simplify the check expression of `THREADS`
h3ssan Aug 19, 2024
67ba81a
Implement `check_required_tools` function
h3ssan Aug 20, 2024
f64efb3
Clarifying the duplicate assigning of `MAILCOW_BACKUP_LOCATION`
h3ssan Aug 20, 2024
cfe13b0
Fix Bug: `backup_and_restore.sh` script, increment `i` only if needed
h3ssan Aug 20, 2024
4d00ca6
Fix: Crash on entering non-numeric value
h3ssan Aug 20, 2024
a2ab527
Fix: backup_location should be exists, for the restore
h3ssan Aug 20, 2024
ea24505
Fix: Crash on entering non-numeric value
h3ssan Aug 20, 2024
c45b860
Implement `declare_restore_point` function
h3ssan Aug 21, 2024
de743cd
Typo in `calling declare_restore_point`
h3ssan Aug 21, 2024
728a995
Fix: `grep: warning: stray \ before :`
h3ssan Aug 21, 2024
d398eca
Implement `declare_file_selection` function
h3ssan Aug 21, 2024
bd229b9
Implement `restore_docker_component` to restore components in one place
h3ssan Aug 21, 2024
d0b38b7
Fix last `grep: warning: stray \ before :`
h3ssan Aug 21, 2024
93fc601
Write a clarification comment for function `restore_docker_component`
h3ssan Aug 21, 2024
78a6266
Introduce new `help` screen, with new flags depends script
h3ssan Aug 22, 2024
92118b4
Use named colors, instead of ANSI codes
h3ssan Aug 22, 2024
a50bf61
Fix: THREADS not printed in `backup_and_restore.sh`
h3ssan Aug 22, 2024
85aa1fb
Fix: Double quote array expansions to avoid re-splitting elements
h3ssan Aug 23, 2024
b7abccc
Fix: mysql component not shown when trying to restore it
h3ssan Aug 27, 2024
9b4bae8
Fix: Make sure there's components before restoring process
h3ssan Aug 27, 2024
cd75642
Add `grep`, `find` and `sed` to the required_tools, also BusyBox check.
h3ssan Aug 27, 2024
4cac270
Remove duplicate threads notice for backup process
h3ssan Aug 27, 2024
48592ab
Implement `--yes` flag, to automate `--delete` and `--backup` script
h3ssan Aug 27, 2024
9d97377
Fix: `find: No such file or directory` when directory not contains data
h3ssan Aug 27, 2024
a20d927
Fix dovecot resync: failed: Connection refused
h3ssan Aug 29, 2024
e8b6eb3
Fix: dovecot resync: failed connection refused
h3ssan Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 106 additions & 66 deletions helper-scripts/backup_and_restore.sh
Original file line number Diff line number Diff line change
@@ -1,156 +1,203 @@
#!/usr/bin/env bash

# ---------------------------------------------------
# Here's only the functions that are optimized,
# checked and refactored.
#
# This comment will be deleted once I finished
# working with this file.
# ---------------------------------------------------

# ----------------- Start Functions -----------------

function print_usage() {
echo "Usage: ${0} [option] [argument]"
echo
echo "Options:"
echo -e " backup\t[crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days]"
echo -e " restore"
echo
echo "Environment Variables:"
echo -e " THREADS\t<num>\tYou can set the thread count with the THREADS environment variable before you run this script."
}

function check_required_tools() {
# Add the required tools to the array
local required_tools=("docker")

for bin in "${required_tools[@]}"; do
if [[ -z $(which ${bin}) ]]; then
echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done

if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
h3ssan marked this conversation as resolved.
Show resolved Hide resolved
echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
exit 1
fi
}

# ----------------- End Functions -----------------

check_required_tools

DEBIAN_DOCKER_IMAGE="mailcow/backup:latest"

if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then
if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then
BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}"
fi

if [[ ! ${1} =~ (backup|restore) ]]; then
echo "First parameter needs to be 'backup' or 'restore'"
print_usage
exit 1
fi

if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then
echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql', 'all' or '--delete-days'"
if [[ -z "${2}" ]]; then
echo -e "\e[31mRequired argument for backup option\e[0m\n"
else
echo -e "\e[31mUnknown argument: ${2}\e[0m\n"
fi

print_usage
exit 1
fi

if [[ -z ${BACKUP_LOCATION} ]]; then
while [[ -z ${BACKUP_LOCATION} ]]; do
if [[ -z "${BACKUP_LOCATION}" ]]; then
while [[ -z "${BACKUP_LOCATION}" ]]; do
read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION
done
fi

if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then
echo "Backup directory needs to be given as absolute path (starting with /)."
if [[ ! "${BACKUP_LOCATION}" =~ ^/ ]]; then
echo -e "\e[31mBackup directory needs to be given as absolute path (starting with /).\e[0m"
exit 1
fi

if [[ -f ${BACKUP_LOCATION} ]]; then
echo "${BACKUP_LOCATION} is a file!"
exit 1
fi

if [[ ! -d ${BACKUP_LOCATION} ]]; then
echo "${BACKUP_LOCATION} is not a directory"
# if "${BACKUP_LOCATION}" not exists, create it.
if [[ ! -e "${BACKUP_LOCATION}" ]]; then
echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m"
read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION
if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then
exit 1
echo -e "\e[33mExiting without creating the backup location.\e[0m"
exit 0
else
mkdir -p ${BACKUP_LOCATION}
chmod 755 ${BACKUP_LOCATION}
mkdir -p "${BACKUP_LOCATION}"
chmod 755 "${BACKUP_LOCATION}"

if [[ ! "${?}" -eq 0 ]]; then
echo -e "\e[31mFailed, check the error above!\e[0m"
exit 1
fi
fi
else
if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then
echo "${BACKUP_LOCATION} is not write-able for others, that's required for a backup."
# if "${BACKUP_LOCATION}" is an exists directory,
# then just check the permissions
elif [[ -d "${BACKUP_LOCATION}" ]]; then
echo -e "\e[32mFound directory ${BACKUP_LOCATION}\e[0m"
if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then
echo -e "\e[31m${BACKUP_LOCATION} is not write-able for others, that's required for a backup.\e[0m"
echo "Execute \`chmod 755 ${BACKUP_LOCATION}\` and try again."
exit 1
fi
# else, the "${BACKUP_LOCATION}" is something else! an alien? yes!
else
echo -e "\e[31m${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e["
exit 1
fi

BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##')
BACKUP_LOCATION=$(echo "${BACKUP_LOCATION}" | sed 's#/$##')
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml
h3ssan marked this conversation as resolved.
Show resolved Hide resolved
ENV_FILE=${SCRIPT_DIR}/../.env
THREADS=$(echo ${THREADS:-1})
ARCH=$(uname -m)

if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then
echo "Thread input is not a number!"
if [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]]; then
echo -e "\e[32mUsing ${THREADS} thread(s) for this run.\e[0m"
else
echo -e "\e[31mThread input is not a number!\e[0m"
exit 1
elif [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then
echo "Using ${THREADS} Thread(s) for this run."
echo "Notice: You can set the Thread count with the THREADS Variable before you run this script."
fi

if [ ! -f ${COMPOSE_FILE} ]; then
echo "Compose file not found"
echo -e "\e[31mCompose file not found\e[0m"
exit 1
fi

if [ ! -f ${ENV_FILE} ]; then
echo "Environment file not found"
echo -e "\e[31mEnvironment file not found\e[0m"
exit 1
fi

echo "Using ${BACKUP_LOCATION} as backup/restore location."
echo -e "\e[33mUsing ${BACKUP_LOCATION} as backup/restore location.\e[0m"
echo

source ${SCRIPT_DIR}/../mailcow.conf

if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then
echo "Could not determine compose project name"
echo -e "\e[31mCould not determine compose project name\e[0m"
exit 1
else
echo "Found project name ${COMPOSE_PROJECT_NAME}"
echo -e "\e[32mFound project name ${COMPOSE_PROJECT_NAME}\e[0m"
CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
fi

if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
>&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
exit 1
fi


function backup() {
DATE=$(date +"%Y-%m-%d-%H-%M-%S")
mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH"
for bin in docker; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done

while (( "$#" )); do
case "$1" in
vmail|all)
docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail
h3ssan marked this conversation as resolved.
Show resolved Hide resolved
;;&
crypt|all)
docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt
;;&
redis|all)
docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis
;;&
rspamd|all)
docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd
;;&
postfix|all)
docker run --name mailcow-backup --rm \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix
;;&
mysql|all)
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
if [[ -z "${SQLIMAGE}" ]]; then
echo "Could not determine SQL image version, skipping backup..."
echo -e "\e[31mCould not determine SQL image version, skipping backup...\e[0m"
shift
continue
else
echo "Using SQL image ${SQLIMAGE}, starting..."
echo -e "\e[32mUsing SQL image ${SQLIMAGE}, starting...\e[0m"
docker run --name mailcow-backup --rm \
--network $(docker network ls -qf name=^${CMPS_PRJ}_mailcow-network$) \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:ro,z \
-t --entrypoint= \
--sysctl net.ipv6.conf.all.disable_ipv6=1 \
-v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
-v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \
${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \
mariabackup --prepare --target-dir=/backup_mariadb ; \
chown -R 999:999 /backup_mariadb ; \
Expand All @@ -160,9 +207,9 @@ function backup() {
--delete-days)
shift
if [[ "${1}" =~ ^[0-9]+$ ]]; then
find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \;
find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \;
else
echo "Parameter of --delete-days is not a number."
echo -e "\e[31mParameter of --delete-days is not a number.\e[0m"
fi
;;
esac
Expand All @@ -171,13 +218,6 @@ function backup() {
}

function restore() {
for bin in docker; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done

if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
COMPOSE_COMMAND="docker compose"

Expand All @@ -190,7 +230,7 @@ function restore() {
fi

echo
echo "Stopping watchdog-mailcow..."
echo -e "\e[33mStopping watchdog-mailcow...\e[0m"
docker stop $(docker ps -qf name=watchdog-mailcow)
echo
RESTORE_LOCATION="${1}"
Expand Down Expand Up @@ -268,11 +308,11 @@ function restore() {
mysql|mariadb)
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
if [[ -z "${SQLIMAGE}" ]]; then
echo "Could not determine SQL image version, skipping restore..."
echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m"
shift
continue
elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then
echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore."
echo -e "\e[31mCould not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.\e[0m"
echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process."
shift
continue
Expand Down Expand Up @@ -339,11 +379,11 @@ if [[ ${1} == "backup" ]]; then
elif [[ ${1} == "restore" ]]; then
i=1
declare -A FOLDER_SELECTION
if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then
echo "Selected backup location has no subfolders"
if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then
echo -e "\e[31mSelected backup location has no subfolders\e[0m"
exit 1
fi
for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do
for folder in $(ls -d "${BACKUP_LOCATION}"/mailcow-*/); do
echo "[ ${i} ] - ${folder}"
FOLDER_SELECTION[${i}]="${folder}"
((i++))
Expand All @@ -358,7 +398,7 @@ elif [[ ${1} == "restore" ]]; then
declare -A FILE_SELECTION
RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}"
if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then
echo "No datasets found"
echo -e "\e[31mNo datasets found\e[0m"
exit 1
fi

Expand Down