From 141dc0cad283f74e0eb9314d0d746b5bcbbe1cee Mon Sep 17 00:00:00 2001 From: Marcel Herrguth Date: Thu, 27 Jan 2022 11:56:02 +0100 Subject: [PATCH] Backport: Fixes #3865, Fixes #3893 - Improve Backup & Restore script handling --- contrib/backup/config.dist | 3 + contrib/backup/functions | 283 +++++++++++++++++++++++- contrib/backup/zammad_backup.sh | 24 +- contrib/backup/zammad_db_user_helper.sh | 26 +++ contrib/backup/zammad_restore.sh | 16 +- 5 files changed, 322 insertions(+), 30 deletions(-) create mode 100644 contrib/backup/zammad_db_user_helper.sh diff --git a/contrib/backup/config.dist b/contrib/backup/config.dist index 221ca9a8d..72ac733fc 100644 --- a/contrib/backup/config.dist +++ b/contrib/backup/config.dist @@ -2,7 +2,10 @@ # # zammad backup script config # +# Learn more about the options below at +# https://docs.zammad.org/en/latest/appendix/backup-and-restore/configuration.html BACKUP_DIR='/var/tmp/zammad_backup' HOLD_DAYS='10' +FULL_FS_DUMP='yes' DEBUG='no' diff --git a/contrib/backup/functions b/contrib/backup/functions index 18afe81f0..7a3627662 100644 --- a/contrib/backup/functions +++ b/contrib/backup/functions @@ -3,6 +3,32 @@ # Zammad backup script functions # +function demand_backup_conf () { + if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then + # Ensure we're inside of our Backup-Script folder (see issue 2508) + # shellcheck disable=SC2164 + cd "${BACKUP_SCRIPT_PATH}" + + # import config + . ${BACKUP_SCRIPT_PATH}/config + else + echo -e "\n The 'config' file is missing!" + echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n" + echo -e " Learn more about the backup configuration at https://docs.zammad.org/en/latest/appendix/backup-and-restore/configuration.html" + exit 1 + fi + + # Check if filesystem full dump setting exists and fall back if not + if [ -z ${FULL_FS_DUMP+x} ]; then + # Falling back to old default behavior + FULL_FS_DUMP='yes' + + if [ "${DEBUG}" == "yes" ]; then + echo "FULL_FS_DUMP is not set, falling back to 'yes' to produce a full backup." + fi + fi +} + function get_zammad_dir () { ZAMMAD_DIR="$(echo ${BACKUP_SCRIPT_PATH} | sed -e 's#/contrib/backup.*##g')" } @@ -30,6 +56,16 @@ function get_db_credentials () { DB_USER="$(grep -m 1 '^[[:space:]]*username:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*username:[[:space:]]*//g')" DB_PASS="$(grep -m 1 '^[[:space:]]*password:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*password:[[:space:]]*//g')" + if [ "${DB_ADAPTER}" == "postgresql" ]; then + # Ensure that HOST and PORT are not empty, provide defaults if needed. + if [ "${DB_HOST}x" == "x" ]; then + DB_HOST="localhost" + fi + if [ "${DB_PORT}x" == "x" ]; then + DB_PORT="5432" + fi + fi + if [ "${DEBUG}" == "yes" ]; then echo "adapter=${DB_ADAPTER} dbhost=${DB_HOST} dbport=${DB_PORT} dbname=${DB_NAME} dbuser=${DB_USER} dbpass=${DB_PASS}" fi @@ -38,14 +74,105 @@ function get_db_credentials () { function backup_dir_create () { test -d ${BACKUP_DIR} || mkdir -p ${BACKUP_DIR} + state=$? + + if [ "${state}" == "1" ]; then + echo -e "\n\n # ERROR(${state}) - Creation of backup directory failed. Please double check permissions." + echo -e " #-> BACKUP WAS NOT SUCCESSFUL" + exit 3 + fi + if [ "${DEBUG}" == "yes" ]; then echo "backup dir is ${BACKUP_DIR}" fi } +backup_file_write_test () { + # We're testing if we can actually write into the provided directory with + # the current user before continuing. + touch ${BACKUP_DIR}/write_test 2> /dev/null + + state=$? + + if [ "${state}" == "1" ]; then + # We're checking for critical restoration errors + # It may not cover all possible errors which is out of scope of this script + echo -e "\n\n # ERROR(${state}) - Creation of backup files was not possible. Double check permissions." + echo -e " #-> BACKUP WAS NOT SUCCESSFUL" + exit 3 + fi + + rm -f ${BACKUP_DIR}/write_test +} + +backup_file_read_test () { + # We're testing if we can read the provided file names before + # starting. Other wise handling would be more difficult depending on + # the installation type + + if [ "${DEBUG}" == "yes" ]; then + echo "I've been looking for these backup files: " + echo "- ${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz" + echo "- ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz" + fi + + if [[ (! -r "${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz") || (! -r "${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz") ]]; then + echo -e "\n\n # ERROR - Cannot read on or more of my backup files. Double check permissions." + echo -e " #-> RESTORE WAS NOT SUCCESSFUL" + exit 3 + fi +} + +function check_empty_password () { + if [ "${DB_PASS}x" == "x" ]; then + echo "# ERROR - Found an empty database password ..." + echo "# - This may be intended or not, however - this script does not support this." + echo "# - If you don't know how to continue, consult https://docs.zammad.org/en/latest/appendix/backup-and-restore/index.html" + exit 2 + fi +} + function backup_files () { echo "creating file backup..." - tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz --exclude='tmp' ${ZAMMAD_DIR#/} + + if [ "${FULL_FS_DUMP}" == 'yes' ]; then + echo " ... as full dump" + tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz\ + --exclude='tmp' --exclude='config/database.yml' ${ZAMMAD_DIR#/} + + state=$? + else + echo " ... only with productive data (attachments)" + + if [ ! -d "${ZAMMAD_DIR}/storage/" ]; then + # Admin has requested an attachment backup only, however, there is no storage + # directory. We'll warn Mr.Admin and create the directory as workaround. + echo " ... WARNING: You don't seem to have any attachments in the file system!" + echo " ... Please consult https://docs.zammad.org/en/latest/appendix/backup-and-restore/troubleshooting.html" + echo " ... Creating empty storage directory so the backup can continue ..." + + mkdir -p ${ZAMMAD_DIR}/storage/ + chown -R zammad:zammad ${ZAMMAD_DIR}/storage/ + fi + + tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz\ + ${ZAMMAD_DIR#/}/storage/\ + + state=$? + fi + + if [ $state == '2' ]; then + echo "# ERROR(2) - File backup reported a fatal error." + echo "- Check file permissions and try again." + echo -e " \n# BACKUP WAS NOT SUCCESSFUL" + exit 1 + fi + + if [ $state == '1' ]; then + echo "# WARNING - Files have changed during backup." + echo "- This indicates your Zammad instance is running and thus may be normal." + fi + ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz ${BACKUP_DIR}/latest_zammad_files.tar.gz } @@ -76,12 +203,29 @@ function backup_db () { --no-privileges --no-owner \ --compress 6 --file "${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz" + state=$? + + if [ "${state}" == "1" ]; then + # We're checking for critical restoration errors + # It may not cover all possible errors which is out of scope of this script + echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." + echo -e " #-> BACKUP WAS NOT SUCCESSFUL" + exit 2 + fi + ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR}/latest_zammad_db.psql.gz else - echo "DB ADAPTER not found. if its sqlite backup is already saved in the filebackup" + echo -e "\n\n # ERROR - Database type or database.yml incorrect. Unsupported database type found." + echo -e " #-> BACKUP WAS NOT SUCCESSFUL" + exit 2 fi } +function backup_chmod_dump_data () { + echo "Ensuring dump permissions ..." + chmod 600 ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz +} + function check_database_config_exists () { if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then get_db_credentials @@ -95,7 +239,9 @@ function restore_warning () { if [ -n "${1}" ]; then CHOOSE_RESTORE="yes" else - echo -e "The restore will delete your current config and database! \nBe sure to have a backup available! \n" + echo -e "The restore will delete your current database! \nBe sure to have a backup available! \n" + echo -e "Please ensure to have twice the storage of the uncompressed backup size! \n\n" + echo -e "Note that the restoration USUALLY requires root permissions as services are stopped! \n\n" echo -e "Enter 'yes' if you want to proceed!" read -p 'Restore?: ' CHOOSE_RESTORE fi @@ -106,6 +252,21 @@ function restore_warning () { fi } +function db_helper_warning () { + echo -e " # WARNING: THIS SCRIPT CHANGES CREDENTIALS, DO NOT CONTINUE IF YOU DON'T KNOW WHAT YOU'RE DOING! \n\n" + + echo -e " Limitations:" + echo -e " - only works for local postgresql installations" + echo -e " - only works for postgresql\n" + echo -e "Enter 'yes' if you want to proceed!" + read -p 'ALTER zammad users password?: ' DB_HELPER + + if [ "${DB_HELPER}" != "yes" ]; then + echo "Helper script aborted!" + exit 1 + fi +} + function get_restore_dates () { RESTORE_FILE_DATES="$(find ${BACKUP_DIR} -type f -iname '*_zammad_files.tar.gz' | sed -e "s#${BACKUP_DIR}/##g" -e "s#_zammad_files.tar.gz##g" | sort)" @@ -173,6 +334,12 @@ function start_zammad () { function stop_zammad () { echo "# Stopping Zammad" ${INIT_CMD} stop zammad + + if [ "$?" != "0" ]; then + echo -e "\n\n # WARNING: You don't seem to have administrative permissions!" + echo -e " #-> This may be fine if you're on a source code installation." + echo -e " #-> Please ensure that Zammad is NOT running - otherwise restore will FAIL.\n" + fi } function restore_zammad () { @@ -214,12 +381,39 @@ function restore_zammad () { create_pgpassfile - zcat < ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz \ - | psql ${DB_HOST:+--host $DB_HOST} ${DB_PORT:+--port $DB_PORT} ${DB_USER:+--username $DB_USER} --dbname ${DB_NAME} + # We're removing uncritical dump information that caused "ugly" error + # messages on older script versions. These could safely be ignored. + zcat ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz | \ + sed '/^CREATE EXTENSION IF NOT EXISTS plpgsql/d'| \ + sed '/^COMMENT ON EXTENSION plpgsql/d'| \ + psql -q -b -o /dev/null \ + ${DB_HOST:+--host $DB_HOST} ${DB_PORT:+--port $DB_PORT} ${DB_USER:+--username $DB_USER} --dbname ${DB_NAME} + + state=$? + + if [[ ("${state}" == "1") || ( "${state}" == "2") || ( "${state}" == "3") ]]; then + # We're checking for critical restoration errors + # It may not cover all possible errors which is out of scope of this script + echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." + echo -e " #-> RESTORE WAS NOT SUCCESSFUL" + exit 2 + fi elif [ "${DB_ADAPTER}" == "mysql2" ]; then echo "# Restoring MySQL DB" zcat < ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz | mysql -u${DB_USER} -p${DB_PASS} ${DB_NAME} + + state=$? + + if [ "${state}" != "0" ]; then + echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." + echo -e " #-> RESTORE WAS NOT SUCCESSFUL" + exit 2 + fi + else + echo -e "\n\n # ERROR - Database type or database.yml incorrect. Unsupported database type found." + echo -e " #-> RESTORE WAS NOT SUCCESSFUL" + exit 2 fi if command -v zammad > /dev/null; then @@ -239,16 +433,91 @@ function restore_zammad () { function restore_files () { echo "# Restoring Files" tar -C / --overwrite -xzf ${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz - echo "# Ensuring correct file rights ..." + + state=$? + + if [[ ($state == '1') || ($state == '2') ]]; then + echo "# ERROR(${state}) - File restore reported an error." + echo "- Check file permissions, and ensure Zammad IS NOT running, and try again." + echo -e " \n# RESTORE WAS NOT SUCCESSFUL" + exit 1 + fi + + echo "# Ensuring correct file permissions ..." chown -R zammad:zammad ${ZAMMAD_DIR} } +function kind_exit () { + # We're nice to our admin and bring Zammad back up before exiting + start_zammad + exit 1 +} + +function db_helper_alter_user () { + # Get DB credentials + get_db_credentials + + if [ "${DB_PASS}x" == "x" ]; then + echo "# Found an empty password - I'll be fixing this for you ..." + + DB_PASS="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c10)" + + sed -e "s/.*adapter:.*/ adapter: ${DB_ADAPTER}/" \ + -e "s/.*username:.*/ username: ${DB_USER}/" \ + -e "s/.*password:.*/ password: ${DB_PASS}/" \ + -e "s/.*database:.*/ database: ${DB_NAME}/" < ${ZAMMAD_DIR}/contrib/packager.io/database.yml.pkgr > ${ZAMMAD_DIR}/config/database.yml + + echo "# ... Fixing permission database.yml" + chown zammad:zammad ${ZAMMAD_DIR}/config/database.yml + fi + + if [ "${DB_USER}x" == "x" ]; then + + echo "ERROR - Your configuration file does not seem to contain a username." + echo "Aborting the script - double check your installation." + + kind_exit + fi + + if [ "${DB_ADAPTER}" == "postgresql" ]; then + + if [[ ("${DB_HOST}" == "localhost") || "${DB_HOST}" == "127.0.0.1" || "${DB_HOST}" == "::1" ]]; then + # Looks like a local pgsql installation - let's continue + su -c "psql -c \"ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';\"" postgres + state=$? + + else + echo "You don't seem to be using a local PostgreSQL installation." + echo "This script does not support your installation. No changes were done." + + kind_exit + fi + + else + echo "You don't seem to use PostgreSQL. This script does not support your installation." + echo "No changes were done." + + kind_exit + fi + + if [ "${state}" != "0" ]; then + echo "ERROR - Our previous command returned an unhandled error code." + echo "Check above command output. Please consult https://community.zammad.org if you can't solve this issue on your own." + + kind_exit + fi +} + function start_backup_message () { echo -e "\n# Zammad backup started - $(date)!\n" } function start_restore_message () { - echo -e "\n# Zammad restored started - $(date)!\n" + echo -e "\n# Zammad restore started - $(date)!\n" +} + +function start_helper_message () { + echo -e "\n # This helper script sets the current Zammad user password on your postgresql server." } function finished_backup_message () { diff --git a/contrib/backup/zammad_backup.sh b/contrib/backup/zammad_backup.sh index dd2ce68b1..31c34791b 100755 --- a/contrib/backup/zammad_backup.sh +++ b/contrib/backup/zammad_backup.sh @@ -6,22 +6,12 @@ # shellcheck disable=SC2046 BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))" -if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then - # Ensure we're inside of our Backup-Script folder (see issue 2508) - # shellcheck disable=SC2164 - cd "${BACKUP_SCRIPT_PATH}" - - # import config - . ${BACKUP_SCRIPT_PATH}/config -else - echo -e "\n The 'config' file is missing!" - echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n" - exit 1 -fi - # import functions . ${BACKUP_SCRIPT_PATH}/functions +# ensure we have all options +demand_backup_conf + # exec backup start_backup_message @@ -29,14 +19,20 @@ get_zammad_dir check_database_config_exists -delete_old_backups +check_empty_password get_backup_date backup_dir_create +backup_file_write_test + +delete_old_backups + backup_files backup_db +backup_chmod_dump_data + finished_backup_message diff --git a/contrib/backup/zammad_db_user_helper.sh b/contrib/backup/zammad_db_user_helper.sh new file mode 100644 index 000000000..f5a94fdcf --- /dev/null +++ b/contrib/backup/zammad_db_user_helper.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# This little helper script + +# shellcheck disable=SC2046 +BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))" + +# import functions +. ${BACKUP_SCRIPT_PATH}/functions + +# exec backup +start_helper_message + +get_zammad_dir + +db_helper_warning + +check_database_config_exists + +detect_initcmd + +stop_zammad + +db_helper_alter_user + +start_zammad diff --git a/contrib/backup/zammad_restore.sh b/contrib/backup/zammad_restore.sh index 5a327fa43..9048fa921 100755 --- a/contrib/backup/zammad_restore.sh +++ b/contrib/backup/zammad_restore.sh @@ -6,18 +6,12 @@ # shellcheck disable=SC2046 BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))" -if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then - # import config - . ${BACKUP_SCRIPT_PATH}/config -else - echo -e "\n The 'config' file is missing!" - echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n" - exit 1 -fi - # import functions . ${BACKUP_SCRIPT_PATH}/functions +# ensure we have all options +demand_backup_conf + # exec restore start_restore_message @@ -27,10 +21,14 @@ restore_warning "${1}" check_database_config_exists +check_empty_password + get_restore_dates choose_restore_date "${1}" +backup_file_read_test + detect_initcmd stop_zammad