README
· 893 B · Text
Raw
Last change: 2025-09-10 Marcel Herrguth
As shown on https://youtu.be/P-Pr7Dy6mP8
Attribution back to this GIST is required!
Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup
Only PostGreSQL is supported; No symlinks are supported
Restoration only works on a pre-installed Zammad installation of the same version (or higher).
During Restore, this script will remove the existing (local) Zammad database without any warning; if it's an external one, ensure it's empty.
COMMAND REFERENCE
- backup: creates a new snapshot and backs up database and storage.
- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used.
- list_snapshots: List all available snapshot IDs.
- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed.
| 1 | Last change: 2025-09-10 Marcel Herrguth |
| 2 | As shown on https://youtu.be/P-Pr7Dy6mP8 |
| 3 | Attribution back to this GIST is required! |
| 4 | |
| 5 | Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup |
| 6 | Only PostGreSQL is supported; No symlinks are supported |
| 7 | |
| 8 | Restoration only works on a pre-installed Zammad installation of the same version (or higher). |
| 9 | During Restore, this script will remove the existing (local) Zammad database without any warning; if it's an external one, ensure it's empty. |
| 10 | |
| 11 | |
| 12 | COMMAND REFERENCE |
| 13 | - backup: creates a new snapshot and backs up database and storage. |
| 14 | - restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used. |
| 15 | - list_snapshots: List all available snapshot IDs. |
| 16 | - verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed. |
restic_zammad.sh
· 13 KiB · Bash
Raw
#!/usr/bin/env bash
#
# 2025-09-06 Marcel Herrguth
# As shown on https://youtu.be/P-Pr7Dy6mP8
# Attribution back to this GIST is required!
#
# Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup
# Only PostGreSQL is supported; No symlinks are supported
#
# Restoration only works on a pre-installed Zammad installation of the same version (or higher).
# During Restore, this script will remove the existing (local) Zammad database without any warning; if it's an external one, ensure it's empty.
#----- CONFIG -------------------------------------------------------------------------------#
# Use either a directory or a supported repository type (like s3:https//domain.tld/bucket)
BACKUP_REPOSITORY='s3:https://s3.domain.tld/resticsample'
# Zammad Installation directory
ZAMMAD_DIR='/opt/zammad'
# Backup either the whole Zammad directory (yes) or just the storage folder (attachments; no)
# Storage folder only is best practice as per documentation
# https://docs.zammad.org/en/latest/appendix/backup-and-restore/index.html
FULL_FS_DUMP='no'
# How many daily and hourly snapshot should stay available?
HOLD_DAYS=14
HOLD_HOURLY=1
# Do you want to cause downtime during backup? (This is the safest backup way)
STOP_DURING_BACKUP='yes'
#----- CONFIG END ---------------------------------------------------------------------------#
# ---- Magic --------------------------------------------------------------------------------#
function pre_flight () {
# Verify that we have all we need.
if [[ ""$BACKUP_REPOSITORY"" == s3:* ]]; then
if [[ -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ]]; then
echo "You have chosen to backup to S3 (it seems), but either AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY."
exit 1
fi
else
echo "This script has been tested with S3 only. This should be fine."
echo "Ensure that the required environment information for restic are available as per their documentation:"
echo "https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html"
fi
if [[ -z "${RESTIC_PASSWORD}" ]]; then
echo "You forgot to set RESTIC_PASSWORD which is mandatory for your backup repository!"
exit 1
fi
}
function start_zammad () {
echo "# Starting Zammad"
systemctl start zammad
}
function stop_zammad () {
echo "# Stopping Zammad"
systemctl stop zammad
}
function get_db_credentials () {
DB_HOST="$(grep -m 1 '^[[:space:]]*host:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*host:[[:space:]]*//g')"
DB_PORT="$(grep -m 1 '^[[:space:]]*port:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*port:[[:space:]]*//g')"
DB_NAME="$(grep -m 1 '^[[:space:]]*database:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*database:[[:space:]]* //g')"
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')"
# 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
}
function create_pgpassfile() {
PGPASSFILE=$(mktemp)
export PGPASSFILE
chmod 600 "$PGPASSFILE"
cat <<EOT > "$PGPASSFILE"
*:*:${DB_NAME}:${DB_USER}:${DB_PASS}
EOT
trap 'rm "$PGPASSFILE"' EXIT
}
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/.*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_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."
echo "Please ensure that your database.yml contains a database user password and that the users password fits."
kind_exit
fi
if [ "${state}" != "0" ]; then
echo "ERROR - Our previous command returned an unhandled error code."
kind_exit
fi
}
function check_for_empty_password() {
if [ "${DB_PASS}x" == "x" ]; then
echo "# ERROR - Found an empty database password ..."
db_helper_alter_user
exit 2
fi
}
function check_database_config_exists () {
if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then
get_db_credentials
else
echo -e "${ZAMMAD_DIR}/config/database.yml is missing. is Zammad installed and configured yet? \nAborting..."
exit 1
fi
}
function delete_old_backups () {
echo "# Invoking cleanup as per snapshot rules"
restic -r "$BACKUP_REPOSITORY" forget --group-by '' --keep-hourly $HOLD_HOURLY --keep-daily $HOLD_DAYS --prune
}
function write_backup () {
stop_zammad if "${STOP_DURING_BACKUP}x" == 'yesx'
echo "# Creating postgresql backup..."
create_pgpassfile
pg_dump --dbname "${DB_NAME}" \
--username "${DB_USER}" \
${DB_HOST:+--host $DB_HOST} \
${DB_PORT:+--port $DB_PORT} \
--no-privileges --no-owner > /tmp/zammad_db.psql
state=$?
if [ "${state}" == "1" ]; then
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
if [ "${FULL_FS_DUMP}" == 'no' ]; then
BACKUP_DIR="${ZAMMAD_DIR}/storage/"
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 " ... This is FINE if you store your attachments either in database or S3!"
echo " ... Creating empty storage directory so the backup can continue ..."
mkdir -p ${ZAMMAD_DIR}/storage/
chown -R zammad:zammad ${ZAMMAD_DIR}/storage/
fi
else
BACKUP_DIR="${ZAMMAD_DIR}"
fi
restic -r "$BACKUP_REPOSITORY" migrate
restic --no-scan --read-concurrency=25 --pack-size=128 --compression off -r "$BACKUP_REPOSITORY" backup \
--exclude "${BACKUP_DIR}/tmp/*" --exclude "${BACKUP_DIR}/node_modules/" \
--exclude "${BACKUP_DIR}/.git/" --exclude "${BACKUP_DIR}/.yarn/cache/" \
${BACKUP_DIR} /tmp/zammad_db.psql
state=$?
# clean up temporary database dump
rm -f /tmp/zammad_db.psql
start_zammad if "${STOP_DURING_BACKUP}x" == 'yesx'
if [ $state == '1' ]; then
echo "# FATAL - Restic could not create the snapshot."
elif [ $state == '3' ]; then
echo "# WARNING - Files changed during snapshot creation; Snapshot potentially incomplete!"
fi
if [ $state -gt 0 ]; then
echo "# BACKUP WAS NOT SUCCESSFUL"
exit $state
fi
}
function list_available_snapshots () {
pre_flight
echo "# Here's your currently available snapshots in your backup repository:"
restic -r "$BACKUP_REPOSITORY" snapshots
}
function verify_backup_repository_health () {
pre_flight
VERIFY_PERCENTAGE="${1:-100}"
echo "# Checking ${VERIFY_PERCENTAGE} of your repository ..."
restic -r "$BACKUP_REPOSITORY" check --read-data-subset="${VERIFY_PERCENTAGE}%"
}
function get_snapshot_id () {
if [ -n "${1}" ]; then
RESTORE_SNAPSHOT_ID="${1}"
else
# User did not provide snapshot so we'll hard guess 'latest'
RESTORE_SNAPSHOT_ID='latest'
echo "... No Snapshot provided, guessing you want to restore 'latest'!"
for i in {15..1}; do
echo -ne "... You have $i seconds to abort.\r"; sleep 1
done
fi
}
function restore_backup () {
echo "# Checking requirements"
if ! id "zammad" &> /dev/null; then
echo "# ERROR: User 'zammad' is not present, but should be by default! #"
echo "# Restoration was NOT successful, exiting ..."
exit 1
fi
stop_zammad
echo "# ... Dropping current database ${DB_NAME}"
# This step is only needed for the majority of pgsql dumps, as they don't provide a drop table statement which will cause
# relation errors during restoration (as on package installation there's always an initialized database)
if command -v zammad > /dev/null; then
zammad config:set DISABLE_DATABASE_ENVIRONMENT_CHECK=1
zammad run rake db:drop
zammad config:set DISABLE_DATABASE_ENVIRONMENT_CHECK=0
else
DISABLE_DATABASE_ENVIRONMENT_CHECK=1 ${ZAMMAD_DIR}/bin/rake db:drop
fi
echo "# ... Creating database ${DB_NAME} for owner ${DB_USER}"
if command -v zammad > /dev/null; then
# We'll skip this part for docker installations
su -c "psql -c \"CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};\"" postgres
else
${ZAMMAD_DIR}/bin/rake db:create
fi
echo "# Restoring PostgreSQL DB"
create_pgpassfile
# We're removing uncritical dump information that caused "ugly" error
# messages on older script versions. These could safely be ignored.
restic -r "$BACKUP_REPOSITORY" dump latest '/tmp/zammad_db.psql' | \
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
if [ "${FULL_FS_DUMP}" == 'no' ]; then
BACKUP_DIR="${ZAMMAD_DIR}/storage/"
else
BACKUP_DIR="${ZAMMAD_DIR}"
fi
echo "# Restoring Files"
restic -r "$BACKUP_REPOSITORY" restore $RESTORE_SNAPSHOT_ID --include "$BACKUP_DIR" --target /
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}
# Ensure all data is loaded from the restored database and not the cache of the previous system state
echo "# Clearing Cache ..."
if command -v zammad > /dev/null; then
zammad run rails r "Rails.cache.clear"
else
${ZAMMAD_DIR}/bin/rails r "Rails.cache.clear"
fi
echo "# Attempting to execute database migrations"
if command -v zammad > /dev/null; then
zammad run rake db:migrate
else
${ZAMMAD_DIR}/bin/rake db:migrate
fi
start_zammad
}
function start_backup_message () {
echo -e "\n# Backup script started - $(date)!\n"
}
function start_restore_message () {
echo -e "\n# Restore script started - $(date)!\n"
}
function finished_backup_message () {
echo -e "\n# Backup script finished; Check output! - $(date)!\n"
}
function finished_restore_message () {
echo -e "\n# Restore script finished; Check output! - $(date)!\n"
}
function execute_backup () {
pre_flight
start_backup_message
check_database_config_exists
check_for_empty_password
write_backup
delete_old_backups
finished_backup_message
}
function execute_restoration () {
pre_flight
check_database_config_exists
check_for_empty_password
start_restore_message
get_snapshot_id $1
restore_backup
finished_restore_message
}
function command_reference () {
echo "COMMAND REFERENCE"
echo "- backup: creates a new snapshot and backs up database and storage."
echo "- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used."
echo "- list_snapshots: List all available snapshot IDs."
echo "- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed."
}
# ---- Option part and control --------------------------------------------------------------#
case "$1" in
backup)
execute_backup
;;
restore)
execute_restoration $2
;;
list_snapshots)
list_available_snapshots
;;
verify_health)
verify_backup_repository_health "$2"
;;
*)
command_reference
;;
esac
| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # 2025-09-06 Marcel Herrguth |
| 4 | # As shown on https://youtu.be/P-Pr7Dy6mP8 |
| 5 | # Attribution back to this GIST is required! |
| 6 | # |
| 7 | # Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup |
| 8 | # Only PostGreSQL is supported; No symlinks are supported |
| 9 | # |
| 10 | # Restoration only works on a pre-installed Zammad installation of the same version (or higher). |
| 11 | # During Restore, this script will remove the existing (local) Zammad database without any warning; if it's an external one, ensure it's empty. |
| 12 | |
| 13 | #----- CONFIG -------------------------------------------------------------------------------# |
| 14 | # Use either a directory or a supported repository type (like s3:https//domain.tld/bucket) |
| 15 | BACKUP_REPOSITORY='s3:https://s3.domain.tld/resticsample' |
| 16 | |
| 17 | # Zammad Installation directory |
| 18 | ZAMMAD_DIR='/opt/zammad' |
| 19 | |
| 20 | # Backup either the whole Zammad directory (yes) or just the storage folder (attachments; no) |
| 21 | # Storage folder only is best practice as per documentation |
| 22 | # https://docs.zammad.org/en/latest/appendix/backup-and-restore/index.html |
| 23 | FULL_FS_DUMP='no' |
| 24 | |
| 25 | # How many daily and hourly snapshot should stay available? |
| 26 | HOLD_DAYS=14 |
| 27 | HOLD_HOURLY=1 |
| 28 | |
| 29 | # Do you want to cause downtime during backup? (This is the safest backup way) |
| 30 | STOP_DURING_BACKUP='yes' |
| 31 | #----- CONFIG END ---------------------------------------------------------------------------# |
| 32 | |
| 33 | # ---- Magic --------------------------------------------------------------------------------# |
| 34 | function pre_flight () { |
| 35 | # Verify that we have all we need. |
| 36 | if [[ ""$BACKUP_REPOSITORY"" == s3:* ]]; then |
| 37 | if [[ -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ]]; then |
| 38 | echo "You have chosen to backup to S3 (it seems), but either AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY." |
| 39 | exit 1 |
| 40 | fi |
| 41 | else |
| 42 | echo "This script has been tested with S3 only. This should be fine." |
| 43 | echo "Ensure that the required environment information for restic are available as per their documentation:" |
| 44 | echo "https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html" |
| 45 | fi |
| 46 | |
| 47 | if [[ -z "${RESTIC_PASSWORD}" ]]; then |
| 48 | echo "You forgot to set RESTIC_PASSWORD which is mandatory for your backup repository!" |
| 49 | exit 1 |
| 50 | fi |
| 51 | } |
| 52 | |
| 53 | function start_zammad () { |
| 54 | echo "# Starting Zammad" |
| 55 | systemctl start zammad |
| 56 | } |
| 57 | |
| 58 | function stop_zammad () { |
| 59 | echo "# Stopping Zammad" |
| 60 | systemctl stop zammad |
| 61 | } |
| 62 | |
| 63 | function get_db_credentials () { |
| 64 | DB_HOST="$(grep -m 1 '^[[:space:]]*host:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*host:[[:space:]]*//g')" |
| 65 | DB_PORT="$(grep -m 1 '^[[:space:]]*port:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*port:[[:space:]]*//g')" |
| 66 | DB_NAME="$(grep -m 1 '^[[:space:]]*database:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*database:[[:space:]]* //g')" |
| 67 | DB_USER="$(grep -m 1 '^[[:space:]]*username:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*username:[[:space:]]*//g')" |
| 68 | DB_PASS="$(grep -m 1 '^[[:space:]]*password:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*password:[[:space:]]*//g')" |
| 69 | |
| 70 | # Ensure that HOST and PORT are not empty, provide defaults if needed. |
| 71 | if [ "${DB_HOST}x" == "x" ]; then |
| 72 | DB_HOST="localhost" |
| 73 | fi |
| 74 | if [ "${DB_PORT}x" == "x" ]; then |
| 75 | DB_PORT="5432" |
| 76 | fi |
| 77 | } |
| 78 | |
| 79 | function create_pgpassfile() { |
| 80 | PGPASSFILE=$(mktemp) |
| 81 | export PGPASSFILE |
| 82 | chmod 600 "$PGPASSFILE" |
| 83 | cat <<EOT > "$PGPASSFILE" |
| 84 | *:*:${DB_NAME}:${DB_USER}:${DB_PASS} |
| 85 | EOT |
| 86 | trap 'rm "$PGPASSFILE"' EXIT |
| 87 | } |
| 88 | |
| 89 | function kind_exit () { |
| 90 | # We're nice to our admin and bring Zammad back up before exiting |
| 91 | start_zammad |
| 92 | exit 1 |
| 93 | } |
| 94 | |
| 95 | function db_helper_alter_user () { |
| 96 | # Get DB credentials |
| 97 | get_db_credentials |
| 98 | |
| 99 | if [ "${DB_PASS}x" == "x" ]; then |
| 100 | echo "# Found an empty password - I'll be fixing this for you ..." |
| 101 | |
| 102 | DB_PASS="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c10)" |
| 103 | |
| 104 | sed -e "s/.*username:.*/ username: ${DB_USER}/" \ |
| 105 | -e "s/.*password:.*/ password: ${DB_PASS}/" \ |
| 106 | -e "s/.*database:.*/ database: ${DB_NAME}/" < ${ZAMMAD_DIR}/contrib/packager.io/database.yml.pkgr > ${ZAMMAD_DIR}/config/database.yml |
| 107 | |
| 108 | echo "# ... Fixing permission database.yml" |
| 109 | chown zammad:zammad ${ZAMMAD_DIR}/config/database.yml |
| 110 | fi |
| 111 | |
| 112 | if [ "${DB_USER}x" == "x" ]; then |
| 113 | |
| 114 | echo "ERROR - Your configuration file does not seem to contain a username." |
| 115 | echo "Aborting the script - double check your installation." |
| 116 | |
| 117 | kind_exit |
| 118 | fi |
| 119 | |
| 120 | if [[ ("${DB_HOST}" == "localhost") || "${DB_HOST}" == "127.0.0.1" || "${DB_HOST}" == "::1" ]]; then |
| 121 | # Looks like a local pgsql installation - let's continue |
| 122 | su -c "psql -c \"ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';\"" postgres |
| 123 | state=$? |
| 124 | |
| 125 | else |
| 126 | echo "You don't seem to be using a local PostgreSQL installation." |
| 127 | echo "This script does not support your installation. No changes were done." |
| 128 | echo "Please ensure that your database.yml contains a database user password and that the users password fits." |
| 129 | |
| 130 | kind_exit |
| 131 | fi |
| 132 | |
| 133 | if [ "${state}" != "0" ]; then |
| 134 | echo "ERROR - Our previous command returned an unhandled error code." |
| 135 | kind_exit |
| 136 | fi |
| 137 | } |
| 138 | |
| 139 | function check_for_empty_password() { |
| 140 | if [ "${DB_PASS}x" == "x" ]; then |
| 141 | echo "# ERROR - Found an empty database password ..." |
| 142 | db_helper_alter_user |
| 143 | exit 2 |
| 144 | fi |
| 145 | } |
| 146 | |
| 147 | function check_database_config_exists () { |
| 148 | if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then |
| 149 | get_db_credentials |
| 150 | else |
| 151 | echo -e "${ZAMMAD_DIR}/config/database.yml is missing. is Zammad installed and configured yet? \nAborting..." |
| 152 | exit 1 |
| 153 | fi |
| 154 | } |
| 155 | |
| 156 | function delete_old_backups () { |
| 157 | echo "# Invoking cleanup as per snapshot rules" |
| 158 | restic -r "$BACKUP_REPOSITORY" forget --group-by '' --keep-hourly $HOLD_HOURLY --keep-daily $HOLD_DAYS --prune |
| 159 | } |
| 160 | |
| 161 | function write_backup () { |
| 162 | stop_zammad if "${STOP_DURING_BACKUP}x" == 'yesx' |
| 163 | echo "# Creating postgresql backup..." |
| 164 | |
| 165 | create_pgpassfile |
| 166 | |
| 167 | pg_dump --dbname "${DB_NAME}" \ |
| 168 | --username "${DB_USER}" \ |
| 169 | ${DB_HOST:+--host $DB_HOST} \ |
| 170 | ${DB_PORT:+--port $DB_PORT} \ |
| 171 | --no-privileges --no-owner > /tmp/zammad_db.psql |
| 172 | |
| 173 | state=$? |
| 174 | |
| 175 | if [ "${state}" == "1" ]; then |
| 176 | echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." |
| 177 | echo -e " #-> BACKUP WAS NOT SUCCESSFUL" |
| 178 | exit 2 |
| 179 | fi |
| 180 | |
| 181 | if [ "${FULL_FS_DUMP}" == 'no' ]; then |
| 182 | BACKUP_DIR="${ZAMMAD_DIR}/storage/" |
| 183 | |
| 184 | if [ ! -d "${ZAMMAD_DIR}/storage/" ]; then |
| 185 | # Admin has requested an attachment backup only, however, there is no storage |
| 186 | # directory. We'll warn Mr.Admin and create the directory as workaround. |
| 187 | echo " ... WARNING: You don't seem to have any attachments in the file system!" |
| 188 | echo " ... This is FINE if you store your attachments either in database or S3!" |
| 189 | echo " ... Creating empty storage directory so the backup can continue ..." |
| 190 | |
| 191 | mkdir -p ${ZAMMAD_DIR}/storage/ |
| 192 | chown -R zammad:zammad ${ZAMMAD_DIR}/storage/ |
| 193 | fi |
| 194 | else |
| 195 | BACKUP_DIR="${ZAMMAD_DIR}" |
| 196 | fi |
| 197 | |
| 198 | restic -r "$BACKUP_REPOSITORY" migrate |
| 199 | restic --no-scan --read-concurrency=25 --pack-size=128 --compression off -r "$BACKUP_REPOSITORY" backup \ |
| 200 | --exclude "${BACKUP_DIR}/tmp/*" --exclude "${BACKUP_DIR}/node_modules/" \ |
| 201 | --exclude "${BACKUP_DIR}/.git/" --exclude "${BACKUP_DIR}/.yarn/cache/" \ |
| 202 | ${BACKUP_DIR} /tmp/zammad_db.psql |
| 203 | |
| 204 | state=$? |
| 205 | |
| 206 | # clean up temporary database dump |
| 207 | rm -f /tmp/zammad_db.psql |
| 208 | |
| 209 | start_zammad if "${STOP_DURING_BACKUP}x" == 'yesx' |
| 210 | |
| 211 | if [ $state == '1' ]; then |
| 212 | echo "# FATAL - Restic could not create the snapshot." |
| 213 | elif [ $state == '3' ]; then |
| 214 | echo "# WARNING - Files changed during snapshot creation; Snapshot potentially incomplete!" |
| 215 | fi |
| 216 | |
| 217 | if [ $state -gt 0 ]; then |
| 218 | echo "# BACKUP WAS NOT SUCCESSFUL" |
| 219 | exit $state |
| 220 | fi |
| 221 | } |
| 222 | |
| 223 | function list_available_snapshots () { |
| 224 | pre_flight |
| 225 | echo "# Here's your currently available snapshots in your backup repository:" |
| 226 | restic -r "$BACKUP_REPOSITORY" snapshots |
| 227 | } |
| 228 | |
| 229 | function verify_backup_repository_health () { |
| 230 | pre_flight |
| 231 | |
| 232 | VERIFY_PERCENTAGE="${1:-100}" |
| 233 | |
| 234 | echo "# Checking ${VERIFY_PERCENTAGE} of your repository ..." |
| 235 | restic -r "$BACKUP_REPOSITORY" check --read-data-subset="${VERIFY_PERCENTAGE}%" |
| 236 | } |
| 237 | |
| 238 | function get_snapshot_id () { |
| 239 | if [ -n "${1}" ]; then |
| 240 | RESTORE_SNAPSHOT_ID="${1}" |
| 241 | else |
| 242 | # User did not provide snapshot so we'll hard guess 'latest' |
| 243 | RESTORE_SNAPSHOT_ID='latest' |
| 244 | |
| 245 | echo "... No Snapshot provided, guessing you want to restore 'latest'!" |
| 246 | for i in {15..1}; do |
| 247 | echo -ne "... You have $i seconds to abort.\r"; sleep 1 |
| 248 | done |
| 249 | fi |
| 250 | } |
| 251 | |
| 252 | function restore_backup () { |
| 253 | echo "# Checking requirements" |
| 254 | |
| 255 | if ! id "zammad" &> /dev/null; then |
| 256 | echo "# ERROR: User 'zammad' is not present, but should be by default! #" |
| 257 | echo "# Restoration was NOT successful, exiting ..." |
| 258 | exit 1 |
| 259 | fi |
| 260 | |
| 261 | stop_zammad |
| 262 | |
| 263 | echo "# ... Dropping current database ${DB_NAME}" |
| 264 | |
| 265 | # This step is only needed for the majority of pgsql dumps, as they don't provide a drop table statement which will cause |
| 266 | # relation errors during restoration (as on package installation there's always an initialized database) |
| 267 | if command -v zammad > /dev/null; then |
| 268 | zammad config:set DISABLE_DATABASE_ENVIRONMENT_CHECK=1 |
| 269 | zammad run rake db:drop |
| 270 | zammad config:set DISABLE_DATABASE_ENVIRONMENT_CHECK=0 |
| 271 | else |
| 272 | DISABLE_DATABASE_ENVIRONMENT_CHECK=1 ${ZAMMAD_DIR}/bin/rake db:drop |
| 273 | fi |
| 274 | |
| 275 | echo "# ... Creating database ${DB_NAME} for owner ${DB_USER}" |
| 276 | if command -v zammad > /dev/null; then |
| 277 | # We'll skip this part for docker installations |
| 278 | su -c "psql -c \"CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};\"" postgres |
| 279 | else |
| 280 | ${ZAMMAD_DIR}/bin/rake db:create |
| 281 | fi |
| 282 | |
| 283 | echo "# Restoring PostgreSQL DB" |
| 284 | |
| 285 | create_pgpassfile |
| 286 | |
| 287 | # We're removing uncritical dump information that caused "ugly" error |
| 288 | # messages on older script versions. These could safely be ignored. |
| 289 | restic -r "$BACKUP_REPOSITORY" dump latest '/tmp/zammad_db.psql' | \ |
| 290 | sed '/^CREATE EXTENSION IF NOT EXISTS plpgsql/d'| \ |
| 291 | sed '/^COMMENT ON EXTENSION plpgsql/d'| \ |
| 292 | psql -q -b -o /dev/null \ |
| 293 | ${DB_HOST:+--host $DB_HOST} ${DB_PORT:+--port $DB_PORT} ${DB_USER:+--username $DB_USER} --dbname ${DB_NAME} |
| 294 | |
| 295 | state=$? |
| 296 | |
| 297 | if [[ ("${state}" == "1") || ( "${state}" == "2") || ( "${state}" == "3") ]]; then |
| 298 | # We're checking for critical restoration errors |
| 299 | # It may not cover all possible errors which is out of scope of this script |
| 300 | echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." |
| 301 | echo -e " #-> RESTORE WAS NOT SUCCESSFUL" |
| 302 | exit 2 |
| 303 | fi |
| 304 | |
| 305 | if [ "${FULL_FS_DUMP}" == 'no' ]; then |
| 306 | BACKUP_DIR="${ZAMMAD_DIR}/storage/" |
| 307 | else |
| 308 | BACKUP_DIR="${ZAMMAD_DIR}" |
| 309 | fi |
| 310 | |
| 311 | echo "# Restoring Files" |
| 312 | restic -r "$BACKUP_REPOSITORY" restore $RESTORE_SNAPSHOT_ID --include "$BACKUP_DIR" --target / |
| 313 | |
| 314 | state=$? |
| 315 | |
| 316 | if [[ ($state == '1') || ($state == '2') ]]; then |
| 317 | echo "# ERROR(${state}) - File restore reported an error." |
| 318 | echo "- Check file permissions, and ensure Zammad IS NOT running, and try again." |
| 319 | echo -e " \n# RESTORE WAS NOT SUCCESSFUL" |
| 320 | exit 1 |
| 321 | fi |
| 322 | |
| 323 | echo "# Ensuring correct file permissions ..." |
| 324 | chown -R zammad:zammad ${ZAMMAD_DIR} |
| 325 | |
| 326 | # Ensure all data is loaded from the restored database and not the cache of the previous system state |
| 327 | echo "# Clearing Cache ..." |
| 328 | if command -v zammad > /dev/null; then |
| 329 | zammad run rails r "Rails.cache.clear" |
| 330 | else |
| 331 | ${ZAMMAD_DIR}/bin/rails r "Rails.cache.clear" |
| 332 | fi |
| 333 | |
| 334 | echo "# Attempting to execute database migrations" |
| 335 | if command -v zammad > /dev/null; then |
| 336 | zammad run rake db:migrate |
| 337 | else |
| 338 | ${ZAMMAD_DIR}/bin/rake db:migrate |
| 339 | fi |
| 340 | |
| 341 | start_zammad |
| 342 | } |
| 343 | |
| 344 | function start_backup_message () { |
| 345 | echo -e "\n# Backup script started - $(date)!\n" |
| 346 | } |
| 347 | |
| 348 | function start_restore_message () { |
| 349 | echo -e "\n# Restore script started - $(date)!\n" |
| 350 | } |
| 351 | |
| 352 | function finished_backup_message () { |
| 353 | echo -e "\n# Backup script finished; Check output! - $(date)!\n" |
| 354 | } |
| 355 | |
| 356 | function finished_restore_message () { |
| 357 | echo -e "\n# Restore script finished; Check output! - $(date)!\n" |
| 358 | } |
| 359 | |
| 360 | function execute_backup () { |
| 361 | pre_flight |
| 362 | start_backup_message |
| 363 | check_database_config_exists |
| 364 | check_for_empty_password |
| 365 | write_backup |
| 366 | delete_old_backups |
| 367 | finished_backup_message |
| 368 | } |
| 369 | |
| 370 | function execute_restoration () { |
| 371 | pre_flight |
| 372 | check_database_config_exists |
| 373 | check_for_empty_password |
| 374 | start_restore_message |
| 375 | get_snapshot_id $1 |
| 376 | restore_backup |
| 377 | finished_restore_message |
| 378 | } |
| 379 | |
| 380 | function command_reference () { |
| 381 | echo "COMMAND REFERENCE" |
| 382 | echo "- backup: creates a new snapshot and backs up database and storage." |
| 383 | echo "- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used." |
| 384 | echo "- list_snapshots: List all available snapshot IDs." |
| 385 | echo "- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed." |
| 386 | } |
| 387 | |
| 388 | # ---- Option part and control --------------------------------------------------------------# |
| 389 | |
| 390 | case "$1" in |
| 391 | backup) |
| 392 | execute_backup |
| 393 | ;; |
| 394 | |
| 395 | restore) |
| 396 | execute_restoration $2 |
| 397 | ;; |
| 398 | |
| 399 | list_snapshots) |
| 400 | list_available_snapshots |
| 401 | ;; |
| 402 | |
| 403 | verify_health) |
| 404 | verify_backup_repository_health "$2" |
| 405 | ;; |
| 406 | |
| 407 | *) |
| 408 | command_reference |
| 409 | ;; |
| 410 | esac |