123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- #!/bin/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>
- # 2013-2023 Emmanuel Bouthenot <kolter@openics.org>
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # 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
- # }}}
- # {{{ Variables
- # Username to access the PostgreSQL server e.g. dbuser
- USERNAME=postgres
- # Password
- # create a file ${HOME}/.pgpass containing a line like this
- # hostname:*:*:dbuser:dbpass
- # replace hostname with the value of DBHOST and postgres with
- # the value of USERNAME
- # Host name (or IP address) of PostgreSQL server e.g localhost
- DBHOST=localhost
- # Port of PostgreSQL server e.g 5432 (only used if DBHOST != localhost)
- DBPORT=5432
- # List of DBNAMES for Daily/Weekly Backup e.g. "DB1 DB2 DB3"
- DBNAMES="all"
- # pseudo database name used to dump global objects (users, roles, tablespaces)
- GLOBALS_OBJECTS="postgres_globals"
- # Backup directory location e.g /backups
- BACKUPDIR="/var/backups"
- # Email Address to send mail to? (user@domain.com)
- MAILADDR="user@domain.com"
- # ============================================================
- # === ADVANCED OPTIONS ( Read the doc's below for details )===
- #=============================================================
- # List of DBNAMES to EXLUCDE if DBNAMES are set to all (must be in " quotes)
- DBEXCLUDE=""
- # Include CREATE DATABASE in backup?
- 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
- DOWEEKLY=6
- # Which day do you want monthly backups? (default is 1, first day of the month)
- # When set to 0, monthly backups are disabled
- DOMONTHLY=1
- # Backup retention count for daily backups
- # Default is 14 days
- BRDAILY=14
- # Backup retention count for weekly backups
- # Default is 5 weeks
- BRWEEKLY=5
- # Backup retention count for monthly backups
- # Default is 12 months
- BRMONTHLY=12
- # Choose Compression type. (gzip, pigz, bzip2, xz or zstd)
- COMP=gzip
- # Compression options
- COMP_OPTS=
- # OPT string for use with pg_dump (see man pg_dump)
- OPT=""
- # Backup files extension
- EXT="sql"
- # Backup files permission
- PERM=600
- # Encryption settings
- # (inspired by http://blog.altudov.com/2010/09/27/using-openssl-for-asymmetric-encryption-of-backups/)
- #
- # It is recommended to backup into a staging directory, and then use the
- # POSTBACKUP script to sync the encrypted files to the desired location.
- #
- # Encryption uses private/public keys. You can generate the key pairs like the following:
- # openssl req -x509 -nodes -days 100000 -newkey rsa:2048 -keyout backup.key -out backup.crt -subj '/'
- #
- # Decryption:
- # openssl smime -decrypt -in backup.sql.gz.enc -binary -inform DEM -inkey backup.key -out backup.sql.gz
- # Enable encryption
- ENCRYPTION=no
- # Encryption public key (path to the key)
- ENCRYPTION_PUBLIC_KEY=""
- # Encryption Cipher (see enc manpage)
- ENCRYPTION_CIPHER="aes256"
- # Suffix for encyrpted files
- ENCRYPTION_SUFFIX=".enc"
- # Command to run before backups (uncomment to use)
- #PREBACKUP="/etc/postgresql-backup-pre"
- # Command run after backups (uncomment to use)
- #POSTBACKUP="/etc/postgresql-backup-post"
- # }}}
- # {{{ OS Specific
- if [ -f /etc/default/autopostgresqlbackup ]; then
- # shellcheck source=/dev/null
- . /etc/default/autopostgresqlbackup
- fi
- # }}}
- # {{{ Documentation
- #=====================================================================
- # Options documentation
- #=====================================================================
- # Set USERNAME and PASSWORD of a user that has at least SELECT permission to
- # ALL databases.
- #
- # Set the DBHOST option to the server you wish to backup, leave the default to
- # backup "this server". To backup multiple servers make copies of this file and
- # set the options for that server.
- #
- # Put in the list of DBNAMES (Databases) to be backed up. If you would like to
- # backup ALL DBs on the server set DBNAMES="all". If set to "all" then any new
- # DBs will automatically be backed up without needing to modify this backup
- # script when a new DB is created.
- #
- # If the DB you want to backup has a space in the name replace the space with a
- # % e.g. "data base" will become "data%base"
- #
- # You can change the backup storage location to anything you like by using the
- # BACKUPDIR setting.
- #
- # === Advanced options doc's ===
- #
- # If you set DBNAMES="all" you can configure the option DBEXCLUDE. Other wise
- # this option will not be used. This option can be used if you want to backup
- # all dbs, but you want exclude some of them. (eg. if a db is to big).
- #
- # Set CREATE_DATABASE to "yes" (the default) if you want your SQL-Dump to
- # create a database with the same name as the original database when restoring.
- # Saying "no" here will allow your to specify the database name you want to
- # restore your dump into, making a copy of the database by using the dump
- # created with autopostgresqlbackup.
- #
- # Use PREBACKUP and POSTBACKUP to specify Per and Post backup commands
- # or scripts to perform tasks either before or after the backup process.
- #
- #=====================================================================
- # Backup Rotation..
- #=====================================================================
- #
- # Rotation is configurable for each period:
- # - daily (max $BRDAILY backups are keeped)
- # - weekly (max $BRWEEKLY backups are keeped)
- # - monthy (max $BRMONTHLY backups are keeped)
- #
- # }}}
- # {{{ 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="$(echo "${DOMONTHLY}" | sed -r 's/^[0-9]$/0\0/')"
- # 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_FILE="${LOG_DIR}/${NAME}_${DBHOST//\//_}-$(date '+%Y-%m-%d_%Hh%Mm').log"
- # Debug mode
- DEBUG="no"
- # pg_dump options
- if [ -n "${OPT}" ]; then
- IFS=" " read -r -a PG_OPTIONS <<< "${OPT}"
- else
- PG_OPTIONS=()
- fi
- # 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
- # Hostname for LOG information and
- # pg_dump{,all} connection settings
- if [ "${DBHOST}" = "localhost" ]; then
- HOST="$(hostname --fqdn)"
- PG_CONN=()
- else
- HOST="${DBHOST}:${DBPORT}"
- PG_CONN=(--host "${DBHOST}" --port "${DBPORT}")
- fi
- if [ -n "${USERNAME}" ]; then
- PG_CONN+=(--username "${USERNAME}")
- fi
- # }}}
- # {{{ log{,ger,_info,_debug,_warn,_error}()
- logger() {
- local fd line severity reset color
- fd="${1}"
- severity="${2}"
- reset=
- color=
- if [ -n "${TERM}" ]; then
- reset="\e[0m"
- case "${severity}" in
- error)
- color="\e[0;91m"
- ;;
- warn)
- color="\e[0;93m"
- ;;
- debug)
- color="\e[0;96m"
- ;;
- *)
- color="\e[0;94m"
- ;;
- esac
- fi
- while IFS= read -r line ; do
- printf "%s|%s|%s\n" "${fd}" "${severity}" "${line}" >> "${LOG_FILE}"
- if [ "${DEBUG}" = "yes" ]; then
- if [ "${fd}" = "out" ]; then
- printf "${color}%6s${reset}|%s\n" "${severity}" "${line}" >&6
- elif [ "${fd}" = "err" ]; then
- printf "${color}%6s${reset}|%s\n" "${severity}" "${line}" >&7
- fi
- fi
- done
- }
- log() {
- echo "$@" | logger "out" ""
- }
- log_debug() {
- echo "$@" | logger "out" "debug"
- }
- log_info() {
- echo "$@" | logger "out" "info"
- }
- log_error() {
- echo "$@" | logger "err" "error"
- }
- log_warn() {
- echo "$@" | logger "err" "warn"
- }
- # }}}
- # {{{ dblist()
- dblist () {
- local cmd_prog cmd_args raw_dblist dblist dbexcl databases
- cmd_prog="psql"
- cmd_args=(-t -l -A -F:)
- if [ "${#PG_CONN[@]}" -gt 0 ]; then
- cmd_args+=("${PG_CONN[@]}")
- fi
- log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
- raw_dblist=$(
- if [ -n "${SU_USERNAME}" ]; then
- su - "${SU_USERNAME}" -l -c "${cmd_prog} ${cmd_args[*]}"
- else
- "${cmd_prog}" "${cmd_args[@]}"
- fi
- )
- read -r -a dblist <<< "$(
- printf "%s" "${raw_dblist}" | \
- sed -r -n 's/^([^:]+):.+$/\1/p' | \
- tr '\n' ' '
- )"
- log_debug "Automatically found databases: ${dblist[*]}"
- if [ -n "${DBEXCLUDE}" ]; then
- IFS=" " read -r -a dbexcl <<< "${DBEXCLUDE}"
- else
- dbexcl=()
- fi
- dbexcl+=(template0)
- log_debug "Excluded databases: ${dbexcl[*]}"
- mapfile -t databases < <(
- comm -23 \
- <(IFS=$'\n'; echo "${dblist[*]}" | sort) \
- <(IFS=$'\n'; echo "${dbexcl[*]}" | sort) \
- )
- databases+=("${GLOBALS_OBJECTS}")
- log_debug "Database(s) to be backuped: ${databases[*]}"
- printf "%s " "${databases[@]}"
- }
- # }}}
- # {{{ dbdump()
- dbdump () {
- local db cmd_prog cmd_args pg_args
- db="${1}"
- pg_args="${PG_OPTIONS[*]}"
- if [ "${db}" = "${GLOBALS_OBJECTS}" ]; then
- cmd_prog="pg_dumpall"
- cmd_args=(--globals-only)
- else
- cmd_prog="pg_dump"
- cmd_args=("${DB}")
- if [ "${CREATE_DATABASE}" = "yes" ]; then
- pg_args+=(--create)
- fi
- fi
- if [ "${#PG_CONN[@]}" -gt 0 ]; then
- cmd_args+=("${PG_CONN[@]}")
- fi
- if [ "${#pg_args[@]}" -gt 0 ]; then
- cmd_args+=("${pg_args[@]}")
- fi
- log_debug "Running command: ${cmd_prog} ${cmd_args[*]}"
- if [ -n "${SU_USERNAME}" ]; then
- su - "${SU_USERNAME}" -l -c "${cmd_prog} ${cmd_args[*]}"
- else
- "${cmd_prog}" "${cmd_args[@]}"
- fi
- }
- # }}}
- # {{{ encryption()
- encryption() {
- log_debug "Encrypting using cypher ${ENCRYPTION_CIPHER} and public key ${ENCRYPTION_PUBLIC_KEY}"
- openssl smime -encrypt -${ENCRYPTION_CIPHER} -binary -outform DEM "${ENCRYPTION_PUBLIC_KEY}" 2>&7
- }
- # }}}
- # {{{ compression()
- compression () {
- if [ -n "${COMP_OPTS}" ]; then
- IFS=" " read -r -a comp_args <<< "${COMP_OPTS}"
- log_debug "Compressing using '${COMP} ${comp_args[*]}'"
- "${COMP}" "${comp_args[@]}" 2>&7
- else
- log_debug "Compressing using '${COMP}'"
- "${COMP}" 2>&7
- fi
- }
- # }}}
- # {{{ dump()
- dump() {
- local db_name dump_file comp_ext
- db_name="${1}"
- dump_file="${2}"
- if [ -n "${COMP}" ]; then
- comp_ext=".comp"
- case "${COMP}" in
- gzip|pigz)
- comp_ext=".gz"
- ;;
- bzip2)
- comp_ext=".bz2"
- ;;
- xz)
- comp_ext=".xz"
- ;;
- zstd)
- comp_ext=".zstd"
- ;;
- esac
- dump_file="${dump_file}${comp_ext}"
- fi
- if [ "${ENCRYPTION}" = "yes" ]; then
- dump_file="${dump_file}${ENCRYPTION_SUFFIX}"
- fi
- if [ -n "${COMP}" ] && [ "${ENCRYPTION}" = "yes" ]; then
- log_debug "Dumping (${db_name}) +compress +encrypt to '${dump_file}'"
- dbdump "${db_name}" | compression | encryption > "${dump_file}"
- elif [ -n "${COMP}" ]; then
- log_debug "Dumping (${db_name}) +compress to '${dump_file}'"
- dbdump "${db_name}" | compression > "${dump_file}"
- elif [ "${ENCRYPTION}" = "yes" ]; then
- log_debug "Dumping (${db_name}) +encrypt to '${dump_file}'"
- dbdump "${db_name}" | encryption > "${dump_file}"
- else
- log_debug "Dumping (${db_name}) to '${dump_file}'"
- dbdump "${db_name}" > "${dump_file}"
- fi
- if [ -f "${dump_file}" ]; then
- log_debug "Fixing permissions (${PERM}) on '${dump_file}'"
- chmod "${PERM}" "${dump_file}"
- if [ ! -s "${dump_file}" ]; then
- log_error "Something went wrong '${dump_file}' is empty (no space left on device?)"
- fi
- else
- log_error "Something went wrong '${dump_file}' does not exists (error during dump?)"
- fi
- }
- # }}}
- # {{{ cleanup()
- cleanup() {
- 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 -r 's/^.+([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}h[0-9]{2}m).*$/\1 \0/' | \
- sort -r | \
- sed -r -n 's/\S+ //p' | \
- tail "+${count}" | \
- xargs -L1 rm -fv | \
- while IFS= read -r line ; do
- log_info "${line}"
- done
- }
- # }}}
- # {{{ usage()
- usage() {
- cat <<EOH
- USAGE: $(basename "$0") [OPTIONS]
- ${NAME} ${VERSION}
- A fully automated tool to make periodic backups of PostgreSQL databases.
- Options:
- -h Shows this help
- -d Run in debug mode (no mail sent)
- EOH
- }
- # }}}
- # {{{ Process command line arguments
- while getopts "hd" OPTION ; do
- case "${OPTION}" in
- h)
- usage
- exit 0
- ;;
- d)
- DEBUG="yes"
- ;;
- *)
- printf "Try \`%s -h\` to check the command line arguments\n" "$(basename "$0")" >&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")
- # }}}
- # {{{ PreBackup
- # Run command before we begin
- if [ -n "${PREBACKUP}" ]; then
- log_info "Prebackup command output:"
- ${PREBACKUP} | \
- while IFS= read -r line ; do
- log " ${line}"
- done
- fi
- # }}}
- # {{{ main()
- log_info "${NAME} version ${VERSION}"
- log_info "Homepage: ${HOMEPAGE}"
- log_info "Backup of Database Server - ${HOST}"
- if [ -n "${COMP}" ]; then
- if ! command -v "${COMP}" >/dev/null ; then
- log_warn "Disabling compression, '${COMP}' command not found"
- unset COMP
- fi
- fi
- if [ "${ENCRYPTION}" = "yes" ] && ! command -v "openssl" >/dev/null ; then
- log_warn "Disabling encryption, 'openssl' command not found"
- ENCRYPTION="no"
- fi
- log_info "Backup Start: $(date)"
- if [ "${DNOM}" = "${DOMONTHLY}" ]; then
- period="monthly"
- rotate="${BRMONTHLY}"
- elif [ "${DNOW}" = "${DOWEEKLY}" ]; then
- period="weekly"
- rotate="${BRWEEKLY}"
- else
- period="daily"
- rotate="${BRDAILY}"
- fi
- # If backing up all DBs on the server
- if [ "${DBNAMES}" = "all" ]; then
- DBNAMES="$(dblist)"
- fi
- for db in ${DBNAMES} ; do
- db="${db//%/ / }"
- log_info "Backup of Database (${period}) '${db}'"
- backupdbdir="${BACKUPDIR}/${period}/${db}"
- if [ ! -e "${backupdbdir}" ]; then
- log_debug "Creating Backup DB directory '${backupdbdir}'"
- mkdir -p "${backupdbdir}"
- fi
- cleanup "${BACKUPDIR}" "${db}" "${period}" "${rotate}"
- backupfile="${backupdbdir}/${db}_${DATE}.${EXT}"
- dump "${db}" "${backupfile}"
- done
- log_info "Backup End: $(date)"
- log_info "Total disk space used for ${BACKUPDIR}: $(du -hs "${BACKUPDIR}" | cut -f1)"
- # }}}
- # {{{ PostBackup
- # Run command when we're done
- if [ -n "${POSTBACKUP}" ]; then
- log_info "Postbackup command output:"
- ${POSTBACKUP} | \
- while IFS= read -r line ; do
- log " ${line}"
- done
- 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.
- # }}}
- # {{{ Reporting
- if [ "${DEBUG}" = "no" ] && grep -q '^err|' "${LOG_FILE}" ; 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")"
- ) | mail -s "${NAME} - log" "${MAILADDR}"
- fi
- # }}}
- # {{{ Cleanup and exit()
- if [ -s "${LOGERR}" ]; then
- rc=1
- else
- rc=0
- fi
- # Clean up log files
- rm -f "${LOG_FILE}"
- exit ${rc}
- # }}}
- # vim: foldmethod=marker foldlevel=0 foldenable
|