|
@@ -1,7 +1,6 @@
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# {{{ License and Copyright
|
|
|
-# PostgreSQL Backup Script
|
|
|
# https://github.com/k0lter/autopostgresqlbackup
|
|
|
# Copyright (c) 2005 Aaron Axelsen <axelseaa@amadmax.com>
|
|
|
# 2005 Friedrich Lobenstock <fl@fl.priv.at>
|
|
@@ -18,100 +17,109 @@
|
|
|
# GNU General Public License for more details.
|
|
|
#
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
-# along with this program; if not, write to the Free Software
|
|
|
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
+# along with this program; if not, see
|
|
|
+# <https://www.gnu.org/licenses/>.
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ Constants
|
|
|
+PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/postgres/bin:/usr/local/pgsql/bin
|
|
|
+HOMEPAGE="https://github.com/k0lter/autopostgresqlbackup"
|
|
|
+NAME="AutoPostgreSQLBackup" # Script name
|
|
|
+VERSION="2.5" # Version Number
|
|
|
+DATE="$(date '+%Y-%m-%d_%Hh%Mm')" # Datestamp e.g 2002-09-21
|
|
|
+DNOW="$(date '+%u')" # Day number of the week 1 to 7 where 1 represents Monday
|
|
|
+DNOM="$(date '+%d')" # Date of the Month e.g. 27
|
|
|
# }}}
|
|
|
|
|
|
# {{{ Variables
|
|
|
|
|
|
+# Configuration file or directory
|
|
|
+CONFIG="/etc/autodbbackup.d"
|
|
|
+
|
|
|
+# Legacy configuration file path (for backward compatibility)
|
|
|
+CONFIG_COMPAT="/etc/default/autopostgresqlbackup"
|
|
|
+
|
|
|
# Email Address to send errors to. If empty errors are displayed on stdout.
|
|
|
MAILADDR="root"
|
|
|
|
|
|
-# By default, on Debian systems (and maybe others), only postgres user is
|
|
|
-# allowed to access PostgreSQL databases without password.
|
|
|
-#
|
|
|
-# In order to dump databases we need to run pg_dump/psql commands as postgres
|
|
|
-# with su. This setting makes possible to run backups with a substitute user
|
|
|
-# using su. If
|
|
|
-#
|
|
|
-# empty, su usage will be disabled)
|
|
|
-SU_USERNAME=
|
|
|
+# Send email only if there are errors
|
|
|
+REPORT_ERRORS_ONLY="yes"
|
|
|
|
|
|
-# Username to access the PostgreSQL server
|
|
|
-USERNAME="postgres"
|
|
|
+# Database engines supported: postgresql, mysql
|
|
|
+DBENGINE="postgresql"
|
|
|
|
|
|
-# Password settings
|
|
|
-# In order to use a password to connect to database create a file
|
|
|
-# ${HOME}/.pgpass containing a line like this
|
|
|
-#
|
|
|
-# hostname:*:*:dbuser:dbpass
|
|
|
-#
|
|
|
-# replace hostname with the value of ${DBHOST}, dbuser with the value of
|
|
|
-# ${USERNAME} and dbpass with the password.
|
|
|
+# Only while using PostgreSQL DB Engine
|
|
|
+SU_USERNAME=""
|
|
|
+
|
|
|
+# Username to access the Database server
|
|
|
+USERNAME=""
|
|
|
|
|
|
-# Host name (or IP address) of PostgreSQL server.
|
|
|
-# Use 'localhost' for socket connection or '127.0.0.1' to force TCP connection
|
|
|
+# Password to access then Database server
|
|
|
+PASSWORD=""
|
|
|
+
|
|
|
+# Host name (or IP address) of the Database server.
|
|
|
DBHOST="localhost"
|
|
|
|
|
|
-# Port of PostgreSQL server.
|
|
|
-# It is also used if ${DBHOST} is localhost (socket connection) as socket name
|
|
|
-# contains port
|
|
|
-DBPORT="5432"
|
|
|
+# Port of Database server.
|
|
|
+DBPORT=""
|
|
|
|
|
|
-# List of database(s) names(s) to backup If you would like to backup all
|
|
|
-# databases on the server set DBNAMES="all".
|
|
|
-#
|
|
|
-# If set to "all" then any new databases will automatically be backed up
|
|
|
-# without needing to modify this settings when a new database is created.
|
|
|
-#
|
|
|
-# If the database you want to backup has a space in the name replace the space
|
|
|
-# with a % ("data base" will become "data%base").
|
|
|
+# List of database(s) names(s) to backup.
|
|
|
DBNAMES="all"
|
|
|
|
|
|
-# List of databases to exclude if DBNAMES is not set to all.
|
|
|
+# List of databases to exclude
|
|
|
DBEXCLUDE=""
|
|
|
|
|
|
-# Pseudo database name used to dump global objects (users, roles, tablespaces)
|
|
|
+# Virtual database name used to dump global objects (users, roles, tablespaces)
|
|
|
GLOBALS_OBJECTS="postgres_globals"
|
|
|
|
|
|
# Backup directory
|
|
|
BACKUPDIR="/var/backups"
|
|
|
|
|
|
-# Include CREATE DATABASE in backups?
|
|
|
+# Include CREATE DATABASE statement
|
|
|
CREATE_DATABASE="yes"
|
|
|
|
|
|
-# Which day do you want weekly backups? (1 to 7 where 1 is Monday)
|
|
|
-# When set to 0, weekly backups are disabled
|
|
|
+# Which day do you want weekly backups?
|
|
|
DOWEEKLY=7
|
|
|
|
|
|
# Which day do you want monthly backups?
|
|
|
-# When set to 0, monthly backups are disabled
|
|
|
DOMONTHLY=1
|
|
|
|
|
|
-# Backup retention count for daily backups, older backups are removed.
|
|
|
+# Backup retention count for daily backups.
|
|
|
BRDAILY=14
|
|
|
|
|
|
-# Backup retention count for weekly backups, older backups are removed.
|
|
|
+# Backup retention count for weekly backups.
|
|
|
BRWEEKLY=5
|
|
|
|
|
|
-# Backup retention count for monthly backups, older backups are removed.
|
|
|
+# Backup retention count for monthly backups.
|
|
|
BRMONTHLY=12
|
|
|
|
|
|
-# Compression tool. It could be gzip, pigz, bzip2, xz, zstd or any compression
|
|
|
-# tool that supports to read data to be compressed from stdin and outputs them
|
|
|
-# to stdout).
|
|
|
-# If the tool is not in ${PATH}, the absolute path can be used.
|
|
|
+# Compression tool.
|
|
|
COMP="gzip"
|
|
|
|
|
|
-# Compression tools options to be used with COMP
|
|
|
+# Compression tools options.
|
|
|
COMP_OPTS=
|
|
|
|
|
|
-# Options string for use with pg_dump (see pg_dump manual page).
|
|
|
+# pg_dump path (pg_dump will be used if empty)
|
|
|
+PGDUMP=
|
|
|
+
|
|
|
+# pg_dumpall path (pg_dumpall will be used if empty)
|
|
|
+PGDUMPALL=
|
|
|
+
|
|
|
+# Options string for use with all_dump (see pg_dump manual page).
|
|
|
PGDUMP_OPTS=
|
|
|
|
|
|
# Options string for use with pg_dumpall (see pg_dumpall manual page).
|
|
|
PGDUMPALL_OPTS=
|
|
|
|
|
|
+# mysql path (mysql will be used if empty)
|
|
|
+MY=
|
|
|
+
|
|
|
+# mysqldump path (mysqldump will be used if empty)
|
|
|
+MYDUMP=
|
|
|
+
|
|
|
+# Options string for use with mysqldump (see myqldump manual page).
|
|
|
+MYDUMP_OPTS=
|
|
|
+
|
|
|
# Backup files extension
|
|
|
EXT="sql"
|
|
|
|
|
@@ -119,25 +127,15 @@ EXT="sql"
|
|
|
PERM=600
|
|
|
|
|
|
# Minimum size (in bytes) for a dump/file (compressed or not).
|
|
|
-# File size below this limit will raise an warning.
|
|
|
MIN_DUMP_SIZE=256
|
|
|
|
|
|
# Enable encryption (asymmetric) with GnuPG.
|
|
|
ENCRYPTION="no"
|
|
|
|
|
|
# Encryption public key (path to the key)
|
|
|
-# Export your public key=""
|
|
|
-# gpg --export 0xY0URK3Y1D --output mypubkey.gpg or \
|
|
|
-# gpg --export --armor 0xY0URK3Y1D --output mypubkey.asc
|
|
|
-# then copy mypubkey.asc or mypubkey.gpg to the path pointed by the
|
|
|
-# ${ENCRYPTION_PUBLIC_KEY}.
|
|
|
-#
|
|
|
-# Decryption
|
|
|
-# gpg --decrypt --output backup.sql.gz backup.sql.gz.enc
|
|
|
-#
|
|
|
ENCRYPTION_PUBLIC_KEY=
|
|
|
|
|
|
-# Suffix for encyrpted files
|
|
|
+# Suffix for encrypted files
|
|
|
ENCRYPTION_SUFFIX=".enc"
|
|
|
|
|
|
# Command or script to execute before backups
|
|
@@ -145,36 +143,6 @@ PREBACKUP=
|
|
|
|
|
|
# Command or script to execute after backups
|
|
|
POSTBACKUP=
|
|
|
-# }}}
|
|
|
-
|
|
|
-# {{{ OS Specific
|
|
|
-if [ -f /etc/default/autopostgresqlbackup ]; then
|
|
|
- # shellcheck source=/dev/null
|
|
|
- . /etc/default/autopostgresqlbackup
|
|
|
-fi
|
|
|
-# }}}
|
|
|
-
|
|
|
-# {{{ Defaults
|
|
|
-PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/postgres/bin:/usr/local/pgsql/bin
|
|
|
-HOMEPAGE="https://github.com/k0lter/autopostgresqlbackup"
|
|
|
-NAME="AutoPostgreSQLBackup" # Script name
|
|
|
-VERSION="2.0" # Version Number
|
|
|
-DATE="$(date '+%Y-%m-%d_%Hh%Mm')" # Datestamp e.g 2002-09-21
|
|
|
-DNOW="$(date '+%u')" # Day number of the week 1 to 7 where 1 represents Monday
|
|
|
-DNOM="$(date '+%d')" # Date of the Month e.g. 27
|
|
|
-LOG_DIR="${BACKUPDIR}" # Directory where the main log is saved
|
|
|
-# Fix day of month (left padding with 0)
|
|
|
-DOMONTHLY="$(printf '%.2i' "${DOMONTHLY}")"
|
|
|
-
|
|
|
-# Using a shared memory filesystem (if available) to avoid
|
|
|
-# issues when there is no left space on backup storage
|
|
|
-if [ -w "/dev/shm" ]; then
|
|
|
- LOG_DIR="/dev/shm"
|
|
|
-fi
|
|
|
-
|
|
|
-LOG_PREFIX="${LOG_DIR}/${NAME}_${DBHOST//\//_}-$(date '+%Y-%m-%d_%Hh%Mm')"
|
|
|
-LOG_FILE="${LOG_PREFIX}.log"
|
|
|
-LOG_REPORT="${LOG_PREFIX}.report"
|
|
|
|
|
|
# Debug mode
|
|
|
DEBUG="no"
|
|
@@ -182,22 +150,8 @@ DEBUG="no"
|
|
|
# Encryption prerequisites
|
|
|
GPG_HOMEDIR=
|
|
|
|
|
|
-# Create required directories
|
|
|
-if [ ! -e "${BACKUPDIR}" ]; then # Check Backup Directory exists.
|
|
|
- mkdir -p "${BACKUPDIR}"
|
|
|
-fi
|
|
|
-
|
|
|
-if [ ! -e "${BACKUPDIR}/daily" ]; then # Check Daily Directory exists.
|
|
|
- mkdir -p "${BACKUPDIR}/daily"
|
|
|
-fi
|
|
|
-
|
|
|
-if [ ! -e "${BACKUPDIR}/weekly" ]; then # Check Weekly Directory exists.
|
|
|
- mkdir -p "${BACKUPDIR}/weekly"
|
|
|
-fi
|
|
|
-
|
|
|
-if [ ! -e "${BACKUPDIR}/monthly" ]; then # Check Monthly Directory exists.
|
|
|
- mkdir -p "${BACKUPDIR}/monthly"
|
|
|
-fi
|
|
|
+# Database connection arguments
|
|
|
+CONN_ARGS=()
|
|
|
|
|
|
# Hostname
|
|
|
HOSTNAME="$(uname -n)"
|
|
@@ -205,12 +159,8 @@ if [[ "${HOSTNAME}" != *.* ]]; then
|
|
|
HOSTNAME="$(hostname --fqdn)"
|
|
|
fi
|
|
|
|
|
|
-HOST="${DBHOST}:${DBPORT}"
|
|
|
-if [ "${DBHOST}" = "localhost" ]; then
|
|
|
- HOST="${HOSTNAME}:${DBPORT} (socket)"
|
|
|
-fi
|
|
|
-
|
|
|
-CONN_ARGS=()
|
|
|
+# Return Code
|
|
|
+RC=0
|
|
|
# }}}
|
|
|
|
|
|
# {{{ log{,ger,_info,_debug,_warn,_error}()
|
|
@@ -273,6 +223,46 @@ log_warn() {
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
+# {{{ arg_encode()
|
|
|
+arg_encode() {
|
|
|
+ while read -r arg ; do
|
|
|
+ echo "${arg}" | sed \
|
|
|
+ -e 's/%/%25/g' \
|
|
|
+ -e 's/ /%20/g' \
|
|
|
+ -e 's/\$/%24/g' \
|
|
|
+ -e 's/`/%60/g' \
|
|
|
+ -e 's/"/%22/g' \
|
|
|
+ -e "s/'/%27/g" \
|
|
|
+ -e "s/#/%23/g" \
|
|
|
+ -e 's/=/%3D/g' \
|
|
|
+ -e 's/\[/%5B/g' \
|
|
|
+ -e 's/\]/%5D/g' \
|
|
|
+ -e 's/!/%21/g' \
|
|
|
+ -e 's/>/%3E/g' \
|
|
|
+ -e 's/</%3C/g' \
|
|
|
+ -e 's/|/%7C/g' \
|
|
|
+ -e 's/;/%3B/g' \
|
|
|
+ -e 's/{/%7B/g' \
|
|
|
+ -e 's/}/%7D/g' \
|
|
|
+ -e 's/(/%28/g' \
|
|
|
+ -e 's/)/%29/g' \
|
|
|
+ -e 's/\*/%2A/g' \
|
|
|
+ -e 's/:/%3A/g' \
|
|
|
+ -e 's/\?/%3F/g' \
|
|
|
+ -e 's/&/%26/g' \
|
|
|
+ -e 's/\//%2F/g'
|
|
|
+ done
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ arg_decode()
|
|
|
+arg_decode() {
|
|
|
+ while read -r arg ; do
|
|
|
+ echo -e "${arg//%/\\x}"
|
|
|
+ done
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
# {{{ gpg_setup()
|
|
|
gpg_setup() {
|
|
|
GPG_HOMEDIR="$(mktemp --quiet --directory -t "${NAME}.XXXXXX")"
|
|
@@ -302,20 +292,30 @@ compression () {
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
-# {{{ pgdb_init()
|
|
|
-pgdb_init () {
|
|
|
+# {{{ postgresqldb_init()
|
|
|
+postgresqldb_init () {
|
|
|
+ if [ -z "${DBPORT}" ]; then
|
|
|
+ DBPORT="5432"
|
|
|
+ fi
|
|
|
CONN_ARGS=(--port "${DBPORT}")
|
|
|
if [ "${DBHOST}" != "localhost" ]; then
|
|
|
CONN_ARGS+=(--host "${DBHOST}")
|
|
|
fi
|
|
|
- if [ -n "${USERNAME}" ]; then
|
|
|
- CONN_ARGS+=(--username "${USERNAME}")
|
|
|
+ if [ -z "${USERNAME}" ]; then
|
|
|
+ USERNAME="postgres"
|
|
|
+ fi
|
|
|
+ CONN_ARGS+=(--username "${USERNAME}")
|
|
|
+ if [ -z "${PGDUMP}" ]; then
|
|
|
+ PGDUMP="pg_dump"
|
|
|
+ fi
|
|
|
+ if [ -z "${PGDUMPALL}" ]; then
|
|
|
+ PGDUMPALL="pg_dumpall"
|
|
|
fi
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
-# {{{ pgdb_list()
|
|
|
-pgdb_list () {
|
|
|
+# {{{ postgresqldb_list()
|
|
|
+postgresqldb_list () {
|
|
|
local cmd_prog cmd_args raw_dblist dblist dbexcl databases
|
|
|
|
|
|
cmd_prog="psql"
|
|
@@ -337,8 +337,9 @@ pgdb_list () {
|
|
|
)
|
|
|
|
|
|
read -r -a dblist <<< "$(
|
|
|
- printf "%s" "${raw_dblist}" | \
|
|
|
+ printf "%s\n" "${raw_dblist}" | \
|
|
|
sed -E -n 's/^([^:]+):.+$/\1/p' | \
|
|
|
+ arg_encode | \
|
|
|
tr '\n' ' '
|
|
|
)"
|
|
|
log_debug "Automatically found databases: ${dblist[*]}"
|
|
@@ -363,8 +364,8 @@ pgdb_list () {
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
-# {{{ pgdb_dump()
|
|
|
-pgdb_dump () {
|
|
|
+# {{{ postgresqldb_dump()
|
|
|
+postgresqldb_dump () {
|
|
|
local db_name cmd_prog cmd_args pg_args
|
|
|
|
|
|
db_name="${1}"
|
|
@@ -383,12 +384,16 @@ pgdb_dump () {
|
|
|
fi
|
|
|
|
|
|
if [ "${db_name}" = "${GLOBALS_OBJECTS}" ]; then
|
|
|
- cmd_prog="pg_dumpall"
|
|
|
+ cmd_prog="${PGDUMPALL}"
|
|
|
cmd_args=(--globals-only)
|
|
|
pg_args=("${PGDUMPALL_ARGS[@]}")
|
|
|
else
|
|
|
- cmd_prog="pg_dump"
|
|
|
- cmd_args=("${db_name}")
|
|
|
+ cmd_prog="${PGDUMP}"
|
|
|
+ if [ -n "${SU_USERNAME}" ]; then
|
|
|
+ cmd_args=("'${db_name}'")
|
|
|
+ else
|
|
|
+ cmd_args=("${db_name}")
|
|
|
+ fi
|
|
|
pg_args=("${PGDUMP_ARGS[@]}")
|
|
|
if [ "${CREATE_DATABASE}" = "yes" ]; then
|
|
|
pg_args+=(--create)
|
|
@@ -413,6 +418,189 @@ pgdb_dump () {
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
+# {{{ mysqldb_init()
|
|
|
+mysqldb_init () {
|
|
|
+ CONN_ARGS=()
|
|
|
+ if [ -z "${DBPORT}" ]; then
|
|
|
+ DBPORT="3306"
|
|
|
+ fi
|
|
|
+ if [ "${DBHOST}" != "localhost" ]; then
|
|
|
+ CONN_ARGS+=(--host "${DBHOST}")
|
|
|
+ fi
|
|
|
+ if [ "${DBPORT}" != "3306" ]; then
|
|
|
+ CONN_ARGS+=(--port "${DBPORT}")
|
|
|
+ fi
|
|
|
+ if [ -z "${USERNAME}" ]; then
|
|
|
+ USERNAME="root"
|
|
|
+ fi
|
|
|
+ CONN_ARGS+=(--user "${USERNAME}")
|
|
|
+ if [ -n "${PASSWORD}" ]; then
|
|
|
+ CONN_ARGS+=(--password "${PASSWORD}")
|
|
|
+ fi
|
|
|
+ if [ -z "${MY}" ]; then
|
|
|
+ MY="mysql"
|
|
|
+ fi
|
|
|
+ if [ -z "${MYDUMP}" ]; then
|
|
|
+ MYDUMP="mysqldump"
|
|
|
+ fi
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ mysqldb_list()
|
|
|
+mysqldb_list () {
|
|
|
+ local cmd_prog cmd_args raw_dblist dblist dbexcl databases
|
|
|
+
|
|
|
+ cmd_prog="${MY}"
|
|
|
+ cmd_args=(--batch --skip-column-names --execute 'SHOW DATABASES;')
|
|
|
+
|
|
|
+ if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
|
|
|
+ cmd_args+=("${CONN_ARGS[@]}")
|
|
|
+ fi
|
|
|
+
|
|
|
+ log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
|
|
|
+ raw_dblist=$(
|
|
|
+ if ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
|
|
|
+ log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
|
|
|
+ fi
|
|
|
+ )
|
|
|
+
|
|
|
+ read -r -a dblist <<< "$(
|
|
|
+ printf "%s\n" "${raw_dblist}" | \
|
|
|
+ arg_encode | \
|
|
|
+ tr '\n' ' '
|
|
|
+ )"
|
|
|
+ log_debug "Automatically found databases: ${dblist[*]}"
|
|
|
+
|
|
|
+ if [ -n "${DBEXCLUDE}" ]; then
|
|
|
+ IFS=" " read -r -a dbexcl <<< "${DBEXCLUDE}"
|
|
|
+ else
|
|
|
+ dbexcl=()
|
|
|
+ fi
|
|
|
+ dbexcl+=(information_schema performance_schema mysql)
|
|
|
+ log_debug "Excluded databases: ${dbexcl[*]}"
|
|
|
+
|
|
|
+ mapfile -t databases < <(
|
|
|
+ comm -23 \
|
|
|
+ <(IFS=$'\n'; echo "${dblist[*]}" | sort) \
|
|
|
+ <(IFS=$'\n'; echo "${dbexcl[*]}" | sort) \
|
|
|
+ )
|
|
|
+ log_debug "Database(s) to be backuped: ${databases[*]}"
|
|
|
+
|
|
|
+ printf "%s " "${databases[@]}"
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ mysqldb_dump()
|
|
|
+mysqldb_dump () {
|
|
|
+ local db_name cmd_prog cmd_args my_args
|
|
|
+
|
|
|
+ db_name="${1}"
|
|
|
+
|
|
|
+ if [ -n "${MYDUMP_OPTS}" ]; then
|
|
|
+ IFS=" " read -r -a MYDUMP_ARGS <<< "${MYDUMP_OPTS}"
|
|
|
+ else
|
|
|
+ MYDUMP_ARGS=()
|
|
|
+ fi
|
|
|
+
|
|
|
+ cmd_prog="${MYDUMP}"
|
|
|
+ cmd_args=("${db_name}")
|
|
|
+ my_args=("${MYDUMP_ARGS[@]}")
|
|
|
+ my_args+=(--quote-names --events --routines)
|
|
|
+ if [ "${CREATE_DATABASE}" = "no" ]; then
|
|
|
+ my_args+=(--databases)
|
|
|
+ else
|
|
|
+ my_args+=(--no-create-db)
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [ "${#CONN_ARGS[@]}" -gt 0 ]; then
|
|
|
+ cmd_args+=("${CONN_ARGS[@]}")
|
|
|
+ fi
|
|
|
+ if [ "${#my_args[@]}" -gt 0 ]; then
|
|
|
+ cmd_args+=("${my_args[@]}")
|
|
|
+ fi
|
|
|
+
|
|
|
+ log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
|
|
|
+ if ! "${cmd_prog}" "${cmd_args[@]}" 2> >(logger "err" "error"); then
|
|
|
+ log_error "Running command '${cmd_prog} ${cmd_args[*]}' has failed"
|
|
|
+ fi
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ db_init()
|
|
|
+db_init () {
|
|
|
+ case ${DBENGINE} in
|
|
|
+ postgresql)
|
|
|
+ postgresqldb_init
|
|
|
+ ;;
|
|
|
+ mysql)
|
|
|
+ mysqldb_init
|
|
|
+ ;;
|
|
|
+ *)
|
|
|
+ log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
|
|
|
+ return 1
|
|
|
+ ;;
|
|
|
+ esac
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ db_list()
|
|
|
+db_list () {
|
|
|
+ case ${DBENGINE} in
|
|
|
+ postgresql)
|
|
|
+ postgresqldb_list
|
|
|
+ ;;
|
|
|
+ mysql)
|
|
|
+ mysqldb_list
|
|
|
+ ;;
|
|
|
+ *)
|
|
|
+ log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
|
|
|
+ return 1
|
|
|
+ ;;
|
|
|
+ esac
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ db_dump()
|
|
|
+db_dump () {
|
|
|
+ case ${DBENGINE} in
|
|
|
+ postgresql|mysql)
|
|
|
+ ${DBENGINE}db_dump "${1}"
|
|
|
+ ;;
|
|
|
+ *)
|
|
|
+ log_error "Unsupported database engine ${DBENGINE}, check DBENGINE configuration parameter"
|
|
|
+ return 1
|
|
|
+ ;;
|
|
|
+ esac
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ db_purge()
|
|
|
+db_purge() {
|
|
|
+ local dumpdir db when count line
|
|
|
+
|
|
|
+ dumpdir="${1}"
|
|
|
+ db="${2}"
|
|
|
+ when="${3}"
|
|
|
+ count="${4}"
|
|
|
+
|
|
|
+ # Since version >= 2.0 the dump filename no longer contains the week number
|
|
|
+ # or the abbreviated month name so in order to be sure to remove the older
|
|
|
+ # dumps we need to sort the filename on the datetime part (YYYY-MM-DD_HHhMMm)
|
|
|
+
|
|
|
+ log_info "Rotating ${count} ${when} backups..."
|
|
|
+ log_debug "Looking for '${db}_*' in '${dumpdir}/${when}/${db}'"
|
|
|
+ find "${dumpdir}/${when}/${db}/" -name "${db}_*" | \
|
|
|
+ sed -E 's/(^.+([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}h[0-9]{2}m).*$)/\2 \1/' | \
|
|
|
+ sort -r | \
|
|
|
+ sed -E -n 's/\S+ //p' | \
|
|
|
+ tail -n "+${count}" | \
|
|
|
+ xargs -L1 rm -fv | \
|
|
|
+ while IFS= read -r line ; do
|
|
|
+ log_info "${line}"
|
|
|
+ done
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
# {{{ dump()
|
|
|
dump() {
|
|
|
local db_name dump_file comp_ext
|
|
@@ -445,16 +633,16 @@ dump() {
|
|
|
|
|
|
if [ -n "${COMP}" ] && [ "${ENCRYPTION}" = "yes" ]; then
|
|
|
log_debug "Dumping (${db_name}) +compress +encrypt to '${dump_file}'"
|
|
|
- pgdb_dump "${db_name}" | compression | encryption > "${dump_file}"
|
|
|
+ db_dump "${db_name}" | compression | encryption > "${dump_file}"
|
|
|
elif [ -n "${COMP}" ]; then
|
|
|
log_debug "Dumping (${db_name}) +compress to '${dump_file}'"
|
|
|
- pgdb_dump "${db_name}" | compression > "${dump_file}"
|
|
|
+ db_dump "${db_name}" | compression > "${dump_file}"
|
|
|
elif [ "${ENCRYPTION}" = "yes" ]; then
|
|
|
log_debug "Dumping (${db_name}) +encrypt to '${dump_file}'"
|
|
|
- pgdb_dump "${db_name}" | encryption > "${dump_file}"
|
|
|
+ db_dump "${db_name}" | encryption > "${dump_file}"
|
|
|
else
|
|
|
log_debug "Dumping (${db_name}) to '${dump_file}'"
|
|
|
- pgdb_dump "${db_name}" > "${dump_file}"
|
|
|
+ db_dump "${db_name}" > "${dump_file}"
|
|
|
fi
|
|
|
|
|
|
if [ -f "${dump_file}" ]; then
|
|
@@ -472,30 +660,118 @@ dump() {
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
-# {{{ cleanup()
|
|
|
+# {{{ setup()
|
|
|
+setup() {
|
|
|
+ # Using a shared memory filesystem (if available) to avoid
|
|
|
+ # issues when there is no left space on backup storage
|
|
|
+ if [ -w "/dev/shm" ]; then
|
|
|
+ LOG_DIR="/dev/shm"
|
|
|
+ fi
|
|
|
+
|
|
|
+ LOG_PREFIX="${LOG_DIR}/${NAME}_${DBHOST//\//_}-$(date '+%Y-%m-%d_%Hh%Mm')"
|
|
|
+ LOG_FILE="${LOG_PREFIX}.log"
|
|
|
+ LOG_REPORT="${LOG_PREFIX}.report"
|
|
|
+
|
|
|
+ HOST="${DBHOST}:${DBPORT}"
|
|
|
+ if [ "${DBHOST}" = "localhost" ]; then
|
|
|
+ HOST="${HOSTNAME}:${DBPORT} (socket)"
|
|
|
+ fi
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ prepare_backupdir()
|
|
|
+prepare_backupdir() {
|
|
|
+ # Create required directories
|
|
|
+ if [ ! -e "${BACKUPDIR}" ]; then # Check Backup Directory exists.
|
|
|
+ mkdir -p "${BACKUPDIR}"
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [ ! -e "${BACKUPDIR}/daily" ]; then # Check Daily Directory exists.
|
|
|
+ mkdir -p "${BACKUPDIR}/daily"
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [ ! -e "${BACKUPDIR}/weekly" ]; then # Check Weekly Directory exists.
|
|
|
+ mkdir -p "${BACKUPDIR}/weekly"
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [ ! -e "${BACKUPDIR}/monthly" ]; then # Check Monthly Directory exists.
|
|
|
+ mkdir -p "${BACKUPDIR}/monthly"
|
|
|
+ fi
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ cleanup()
|
|
|
cleanup() {
|
|
|
- local dumpdir db when count line
|
|
|
+ # Cleanup GnuPG home dir
|
|
|
+ if [ -d "${GPG_HOMEDIR}" ]; then
|
|
|
+ rm -rf "${GPG_HOMEDIR}"
|
|
|
+ fi
|
|
|
|
|
|
- dumpdir="${1}"
|
|
|
- db="${2}"
|
|
|
- when="${3}"
|
|
|
- count="${4}"
|
|
|
+ # Clean up log files
|
|
|
+ rm -f "${LOG_FILE}" "${LOG_REPORT}"
|
|
|
+}
|
|
|
+# }}}
|
|
|
|
|
|
- # Since version >= 2.0 the dump filename no longer contains the week number
|
|
|
- # or the abbreviated month name so in order to be sure to remove the older
|
|
|
- # dumps we need to sort the filename on the datetime part (YYYY-MM-DD_HHhMMm)
|
|
|
+# {{{ setup_io()
|
|
|
+setup_io() {
|
|
|
+ exec 6>&1 # Link file descriptor #6 with stdout.
|
|
|
+ # Saves stdout.
|
|
|
+ exec 7>&2 # Link file descriptor #7 with stderr.
|
|
|
+ # Saves stderr.
|
|
|
+ exec > >( logger "out")
|
|
|
+ exec 2> >( logger "err")
|
|
|
+}
|
|
|
+# }}}
|
|
|
|
|
|
- log_info "Rotating ${count} ${when} backups..."
|
|
|
- log_debug "Looking for '${db}_*' in '${dumpdir}/${when}/${db}'"
|
|
|
- find "${dumpdir}/${when}/${db}/" -name "${db}_*" | \
|
|
|
- sed -E 's/(^.+([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}h[0-9]{2}m).*$)/\2 \1/' | \
|
|
|
- sort -r | \
|
|
|
- sed -E -n 's/\S+ //p' | \
|
|
|
- tail -n "+${count}" | \
|
|
|
- xargs -L1 rm -fv | \
|
|
|
- while IFS= read -r line ; do
|
|
|
- log_info "${line}"
|
|
|
- done
|
|
|
+# {{{ cleanup_io()
|
|
|
+cleanup_io() {
|
|
|
+ exec 1>&6 6>&- # Restore stdout and close file descriptor #6.
|
|
|
+ exec 2>&7 7>&- # Restore stdout and close file descriptor #7.
|
|
|
+}
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ reporting()
|
|
|
+reporting() {
|
|
|
+ local exitcode subject
|
|
|
+
|
|
|
+ exitcode=0
|
|
|
+ if grep -q '^err|' "${LOG_FILE}"; then
|
|
|
+ exitcode=1
|
|
|
+ fi
|
|
|
+
|
|
|
+ if [[ ( "${DEBUG}" = "no" ) && ( ${exitcode} = 1 || "${REPORT_ERRORS_ONLY}" = "no" ) ]]; then
|
|
|
+ (
|
|
|
+ if [ ${exitcode} = 1 ]; then
|
|
|
+ printf "*Errors/Warnings* (below) reported during backup on *%s*:\n\n" "${HOST}"
|
|
|
+ grep '^err|' "${LOG_FILE}" | cut -d '|' -f 3- | \
|
|
|
+ while IFS= read -r line ; do
|
|
|
+ printf " | %s\n" "${line}"
|
|
|
+ done
|
|
|
+ fi
|
|
|
+ printf "\n\nFull backup log follows:\n\n"
|
|
|
+ grep -v '^...|debug|' "${LOG_FILE}" | \
|
|
|
+ while IFS="|" read -r fd level line ; do
|
|
|
+ if [ -n "${level}" ]; then
|
|
|
+ printf "%8s| %s\n" "*${level}*" "${line}"
|
|
|
+ else
|
|
|
+ printf "%8s| %s\n" "" "${line}"
|
|
|
+ fi
|
|
|
+ done
|
|
|
+ printf "\nFor more information, try to run %s in debug mode, see \`%s -h\`\n" "${NAME}" "$(basename "$0")"
|
|
|
+ ) > "${LOG_REPORT}"
|
|
|
+
|
|
|
+ if [ -n "${MAILADDR}" ]; then
|
|
|
+ subject="report"
|
|
|
+ if [ ${exitcode} = 1 ]; then
|
|
|
+ subject="issues"
|
|
|
+ fi
|
|
|
+ mail -s "${NAME} ${subject} on ${HOSTNAME}" "${MAILADDR}" < "${LOG_REPORT}"
|
|
|
+ else
|
|
|
+ cat "${LOG_REPORT}"
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ return ${exitcode}
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
@@ -506,18 +782,21 @@ USAGE: $(basename "$0") [OPTIONS]
|
|
|
|
|
|
${NAME} ${VERSION}
|
|
|
|
|
|
-A fully automated tool to make periodic backups of PostgreSQL databases.
|
|
|
+A fully automated tool to make periodic backups databases (supports PostgreSQL and MySQL/MariaDB).
|
|
|
|
|
|
Options:
|
|
|
-h Shows this help
|
|
|
-d Run in debug mode (no mail sent)
|
|
|
+ -c Configuration file or directory (default: ${CONFIG})
|
|
|
+ Note: if ${CONFIG} file or directory does not exists
|
|
|
+ but ${CONFIG_COMPAT} exists, it will be used
|
|
|
+ for backward compatibility.
|
|
|
EOH
|
|
|
}
|
|
|
# }}}
|
|
|
|
|
|
# {{{ Process command line arguments
|
|
|
-
|
|
|
-while getopts "hd" OPTION ; do
|
|
|
+while getopts "hdc:" OPTION ; do
|
|
|
case "${OPTION}" in
|
|
|
h)
|
|
|
usage
|
|
@@ -526,20 +805,66 @@ while getopts "hd" OPTION ; do
|
|
|
d)
|
|
|
DEBUG="yes"
|
|
|
;;
|
|
|
+ c)
|
|
|
+ CONFIG="${OPTARG}"
|
|
|
+ CONFIG_COMPAT=
|
|
|
+ ;;
|
|
|
*)
|
|
|
- printf "Try \`%s -h\` to check the command line arguments\n" "$(basename "$0")" >&2
|
|
|
+ printf "Try \`%s -h\` to check the command line arguments\n" "${NAME}" >&2
|
|
|
exit 1
|
|
|
esac
|
|
|
done
|
|
|
# }}}
|
|
|
|
|
|
# {{{ I/O redirection(s) for logging
|
|
|
-exec 6>&1 # Link file descriptor #6 with stdout.
|
|
|
- # Saves stdout.
|
|
|
-exec 7>&2 # Link file descriptor #7 with stderr.
|
|
|
- # Saves stderr.
|
|
|
-exec > >( logger "out")
|
|
|
-exec 2> >( logger "err")
|
|
|
+setup_io
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ Setup runtime settings
|
|
|
+setup
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ Config file loading
|
|
|
+CONFIG_N=0
|
|
|
+if [ -d "${CONFIG}" ]; then
|
|
|
+ CONFIG_N=$(find "${CONFIG}" -type f -iname '*.conf' | wc -l)
|
|
|
+fi
|
|
|
+
|
|
|
+if [ -f "${CONFIG_COMPAT}" ]; then
|
|
|
+ log_debug "Loading config '${CONFIG_COMPAT}' (for backward compatibility)"
|
|
|
+ # shellcheck source=/dev/null
|
|
|
+ . "${CONFIG_COMPAT}"
|
|
|
+ setup
|
|
|
+elif [ "${CONFIG_N}" -gt 0 ]; then
|
|
|
+ CMD="$(readlink -f "${0}")"
|
|
|
+ CMD_ARGS=()
|
|
|
+
|
|
|
+ if [ "${DEBUG}" = "yes" ]; then
|
|
|
+ CMD_ARGS+=(-d)
|
|
|
+ fi
|
|
|
+
|
|
|
+ cleanup_io
|
|
|
+ cleanup
|
|
|
+
|
|
|
+ find "${CONFIG}" -type f -iname '*.conf' -print0 | \
|
|
|
+ xargs -0 -L1 "${CMD}" "${CMD_ARGS[@]}" -c
|
|
|
+ exit $?
|
|
|
+elif [ -f "${CONFIG}" ]; then
|
|
|
+ log_debug "Loading config '${CONFIG}'"
|
|
|
+ # shellcheck source=/dev/null
|
|
|
+ . "${CONFIG}"
|
|
|
+ setup
|
|
|
+else
|
|
|
+ log_error "${NAME}: config file or directory '${CONFIG}' does not exists or directory '${CONFIG}' does not contains any configuration files."
|
|
|
+ reporting
|
|
|
+ cleanup_io
|
|
|
+ cleanup
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+# }}}
|
|
|
+
|
|
|
+# {{{ Create backup directories
|
|
|
+prepare_backupdir
|
|
|
# }}}
|
|
|
|
|
|
# {{{ PreBackup
|
|
@@ -556,7 +881,7 @@ fi
|
|
|
# {{{ main()
|
|
|
log_info "${NAME} version ${VERSION}"
|
|
|
log_info "Homepage: ${HOMEPAGE}"
|
|
|
-log_info "Backup of Database Server - ${HOST}"
|
|
|
+log_info "Backup of Database Server (${DBENGINE}) - ${HOST}"
|
|
|
|
|
|
if [ -n "${COMP}" ]; then
|
|
|
if ! command -v "${COMP}" >/dev/null ; then
|
|
@@ -601,26 +926,27 @@ else
|
|
|
rotate="${BRDAILY}"
|
|
|
fi
|
|
|
|
|
|
+db_init
|
|
|
+
|
|
|
# If backing up all DBs on the server
|
|
|
if [ "${DBNAMES}" = "all" ]; then
|
|
|
- DBNAMES="$(pgdb_list)"
|
|
|
+ DBNAMES="$(db_list)"
|
|
|
fi
|
|
|
|
|
|
-pgdb_init
|
|
|
+for db_enc in ${DBNAMES} ; do
|
|
|
+ db="$(echo "${db_enc}" | arg_decode)"
|
|
|
|
|
|
-for db in ${DBNAMES} ; do
|
|
|
- db="${db//%/ / }"
|
|
|
log_info "Backup of Database (${period}) '${db}'"
|
|
|
|
|
|
- backupdbdir="${BACKUPDIR}/${period}/${db}"
|
|
|
+ backupdbdir="${BACKUPDIR}/${period}/${db_enc}"
|
|
|
if [ ! -e "${backupdbdir}" ]; then
|
|
|
log_debug "Creating Backup DB directory '${backupdbdir}'"
|
|
|
mkdir -p "${backupdbdir}"
|
|
|
fi
|
|
|
|
|
|
- cleanup "${BACKUPDIR}" "${db}" "${period}" "${rotate}"
|
|
|
+ db_purge "${BACKUPDIR}" "${db_enc}" "${period}" "${rotate}"
|
|
|
|
|
|
- backupfile="${backupdbdir}/${db}_${DATE}.${EXT}"
|
|
|
+ backupfile="${backupdbdir}/${db_enc}_${DATE}.${EXT}"
|
|
|
dump "${db}" "${backupfile}"
|
|
|
done
|
|
|
log_info "Backup End: $(date)"
|
|
@@ -640,54 +966,18 @@ fi
|
|
|
# }}}
|
|
|
|
|
|
# {{{ cleanup I/O redirections
|
|
|
-exec 1>&6 6>&- # Restore stdout and close file descriptor #6.
|
|
|
-exec 2>&7 7>&- # Restore stdout and close file descriptor #7.
|
|
|
+cleanup_io
|
|
|
# }}}
|
|
|
|
|
|
# {{{ Reporting
|
|
|
-if grep -q '^err|' "${LOG_FILE}"; then
|
|
|
- rc=1
|
|
|
-else
|
|
|
- rc=0
|
|
|
-fi
|
|
|
-
|
|
|
-if [ "${DEBUG}" = "no" ] && [ ${rc} = 1 ]; then
|
|
|
- (
|
|
|
- printf "*Errors/Warnings* (below) reported during backup on *%s*:\n\n" "${HOST}"
|
|
|
- grep '^err|' "${LOG_FILE}" | cut -d '|' -f 3- | \
|
|
|
- while IFS= read -r line ; do
|
|
|
- printf " | %s\n" "${line}"
|
|
|
- done
|
|
|
- printf "\n\nFull backup log follows:\n\n"
|
|
|
- grep -v '^...|debug|' "${LOG_FILE}" | \
|
|
|
- while IFS="|" read -r fd level line ; do
|
|
|
- if [ -n "${level}" ]; then
|
|
|
- printf "%8s| %s\n" "*${level}*" "${line}"
|
|
|
- else
|
|
|
- printf "%8s| %s\n" "" "${line}"
|
|
|
- fi
|
|
|
- done
|
|
|
- printf "\nFor more information, try to run %s in debug mode, see \`%s -h\`\n" "${NAME}" "$(basename "$0")"
|
|
|
- ) > "${LOG_REPORT}"
|
|
|
-
|
|
|
- if [ -n "${MAILADDR}" ]; then
|
|
|
- mail -s "${NAME} issues on ${HOSTNAME}" "${MAILADDR}" < "${LOG_REPORT}"
|
|
|
- else
|
|
|
- cat "${LOG_REPORT}"
|
|
|
- fi
|
|
|
-fi
|
|
|
+reporting
|
|
|
+RC=${?}
|
|
|
# }}}
|
|
|
|
|
|
# {{{ Cleanup and exit()
|
|
|
-# Cleanup GnuPG home dir
|
|
|
-if [ -d "${GPG_HOMEDIR}" ]; then
|
|
|
- rm -rf "${GPG_HOMEDIR}"
|
|
|
-fi
|
|
|
-
|
|
|
-# Clean up log files
|
|
|
-rm -f "${LOG_FILE}" "${LOG_REPORT}"
|
|
|
+cleanup
|
|
|
|
|
|
-exit ${rc}
|
|
|
+exit ${RC}
|
|
|
# }}}
|
|
|
|
|
|
# vim: foldmethod=marker foldlevel=0 foldenable
|