#!/bin/bash # DOBAPOR - DumpOrBackupAndPruneOrRestore # (c) 2019 Tobias Strobel DATEFORMAT='%Y-%m-%d-%H-%M-%S' BKPPATH='/root/backup' LOGPATH="${BKPPATH}/log" TMPDIR=$(mktemp -d) HOST=$(hostname) DOMAIN=$(hostname -d) ROOT_DIR='/opt' CONFIG_DIR="${ROOT_DIR}/config" DATA_DIR="${ROOT_DIR}/data" RUNTIME_DIR="${ROOT_DIR}/runtime" ACTIONS=( dump backup restore prune status ) MCSERVICE='vmail|crypt|redis|rspamd|postfix|mysql' SERVICE='rootfs|etc' #DUMPSERVICE='nextcloud|wallabag|mailcow|ttrss|ejabberd' DUMPSERVICE='wallabag|mailcow|ejabberd' CHOOSESERVICE="${SERVICE}|${DUMPSERVICE}|all" CHOOSEMCSERVICE="${MCSERVICE}|all" DB_KEEPLATEST=4 # n+1 LOGS_KEEPLATEST=11 # n+1 #BORG_KEEPWITHIN=2d #BORG_KEEPDAILY=7 #BORG_KEEPWEEKLY=4 #BORG_KEEPMONTHLY=3 BORG_KEEPWITHIN=1H BORG_KEEPDAILY=0 BORG_KEEPWEEKLY=0 BORG_KEEPMONTHLY=0 declare -a BORG_REPOS declare -a BORG_PASSPHRASES BORG_REPOS[1]='/localbackup' BORG_PASSPHRASES[1]='truth plug charcoal spoils thank ladder chaperone scale playmate retiree' BORG_REPOS[2]='rsync.net:borg-aech' BORG_PASSPHRASES[2]='debtor snowcap protrude swinger doozy catchy frenzy shininess denote ferris' # only for Rsync.net users export BORG_REMOTE_PATH=/usr/local/bin/borg1/borg1 if [[ ! ${1} =~ (dump|backup|restore|prune|status) ]]; then echo "First parameter needs to be 'dump|backup|restore|prune|status'" exit 1 elif [[ ${1} =~ (backup|prune) && ! ${2} =~ (${CHOOSESERVICE}) ]]; then echo "Second parameter needs to be '${CHOOSESERVICE}'" exit 1 elif [[ ${1} =~ (dump) && ! ${2} =~ (${DUMPSERVICE}) ]]; then echo "Second parameter needs to be '${DUMPSERVICE}'" exit 1 fi ACTION=${1} if [ ! -d "${BKPPATH}" ]; then mkdir -p ${BKPPATH} fi if [ ! -d "${LOGPATH}" ]; then mkdir -p ${LOGPATH} fi LOGFILE="${LOGPATH}/${ACTION}-$(date +${DATEFORMAT}).log" ## FUNCTIONS ################################################### function dumpDBfromDocker() { if [[ ! ${1} =~ (mysql|postgres) ]]; then echo 'First parameter needs to be mysql or postgres.' exit 1 fi if [[ ${1} =~ (postgres) ]] && [[ ! ${2} =~ (nextcloud|wallabag|ttrss|ejabberd) ]]; then echo 'Second parameter needs to be nextcloud, wallabag, ttrss or ejabberd.' exit 1 fi mkdir -p "${DATA_DIR}/${2}/dumps" chmod 700 -R "${DATA_DIR}/${2}/dumps" echo "Dump ${1} database of ${2}..." case "${1}" in mysql) if [[ ! -f ${CONFIG_DIR}/${2}/${2}.env ]]; then echo "No env file for ${2} found!" exit 1 fi source ${CONFIG_DIR}/${2}/${2}.env SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${CONFIG_DIR}/${2}/docker-compose.yml|head -n1) docker run --rm \ --network $(docker network ls -qf name=${2}_internal) \ -v ${RUNTIME_DIR}/${2}/db:/var/lib/mysql/ \ --entrypoint= \ -v ${DATA_DIR}/${2}/dumps:/dump \ ${SQLIMAGE} /bin/sh -c "mysqldump -h${MYSQL_HOST:-db} -u${MYSQL_USER:-${2}} -p${MYSQL_PASSWORD} --all-databases --single-transaction --quick|gzip -c >/dump/mysql-$(date +${DATEFORMAT}).gz; \ ls -tp /dump/mysql*| grep -v '/$' | tail -n +${DB_KEEPLATEST:-4} | xargs -d '\n' -r rm --" check $? ;;& postgres) if [[ ! -f ${CONFIG_DIR}/${2}/${2}.env ]]; then echo "No env file for ${2} found!" exit 1 fi source ${CONFIG_DIR}/${2}/${2}.env SQLIMAGE=$(grep -iEo 'postgres\:.+' ${CONFIG_DIR}/${2}/docker-compose.yml|head -n1) docker run --rm \ --network $(docker network ls -qf name=${2}_internal) \ -v ${RUNTIME_DIR}/${2}/postgres:/var/lib/postgresql/data \ --entrypoint= \ -v ${DATA_DIR}/${2}/dumps:/dump \ ${SQLIMAGE} /bin/sh -c "pg_dump -Fc postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-${2}} -f /dump/postgres-$(date +${DATEFORMAT}).dump; \ ls -tp /dump/postgres*| grep -v '/$' | tail -n +${DB_KEEPLATEST:-4} | xargs -d '\n' -r rm --" check $? ;; esac } function runBorgBackup() { if [[ ! ${1} =~ (${SERVICE}|${DUMPSERVICE}) ]]; then echo "First parameter needs specify the service: '${SERVICE}'" exit 1 fi if [[ ! -f ${TMPDIR}/fileList.borg ]]; then echo 'No file list for borg is present in tmpdir!' exit 1 fi if [[ ! -f ${TMPDIR}/excludeList.borg ]]; then touch ${TMPDIR}/excludeList.borg fi for ((i=1;i<=${#BORG_REPOS[@]};i++)) do BORG_REPO=${BORG_REPOS[$i]} export BORG_PASSPHRASE=${BORG_PASSPHRASES[$i]} echo "Push backup of ${1} to $BORG_REPO..." borg create -v --stats --compression lz4 --exclude-from ${TMPDIR}/excludeList.borg \ ${BORG_REPO}::${1}'-{now:%Y-%m-%d_%H:%M:%S}' $(<${TMPDIR}/fileList.borg) check $? echo done >${TMPDIR}/excludeList.borg >${TMPDIR}/fileList.borg } function runBorgPrune() { if [[ ! ${1} =~ (${SERVICE}|${DUMPSERVICE}) ]]; then echo "First parameter needs specify the service: '${SERVICE}'" exit 1 fi echo "###### Pruning backup for ${1} on $(date) ######" for ((i=1;i<=${#BORG_REPOS[@]};i++)) do BORG_REPO=${BORG_REPOS[$i]} export BORG_PASSPHRASE=${BORG_PASSPHRASES[$i]} borg prune -v --list --prefix ${1} ${BORG_REPO} \ --keep-within=${BORG_KEEPWITHIN:-2d} \ --keep-daily=${BORG_KEEPDAILY:-7} \ --keep-weekly=${BORG_KEEPWEEKLY:-4} \ --keep-monthly=${BORG_KEEPMONTHLY:-6} check $? echo done } function pruneLogs() { cd ${LOGPATH} for ACT in ${ACTIONS[@]} do find . -maxdepth 1 -name "${ACT}*.log" | tail -n +${LOGS_KEEPLATEST:-6} | xargs -i rm -- {} done } function showBorgStats() { for ((i=1;i<=${#BORG_REPOS[@]};i++)) do BORG_REPO=${BORG_REPOS[$i]} export BORG_PASSPHRASE=${BORG_PASSPHRASES[$i]} #borg info ${1} ${BORG_REPO} echo done } function mailcowBaR () { if [[ ! ${1} =~ (${MCSERVICE}|all) ]]; then echo "First parameter needs to be ${MCSERVICE}|all" exit 1 fi echo "Execute mailcow backup script for ${1}..." MAILCOW_BACKUP_LOCATION=${ROOT_DIR}/data/mailcow/dumps /opt/mailcow-dockerized/helper-scripts/backup_and_restore.sh backup ${1} >/dev/null check $? cd ${ROOT_DIR}/data/mailcow/dumps/; ls -1 -d "$PWD/"* -t | tail -n +${DB_KEEPLATEST:-4} | xargs -i rm -r -- {} } function nextcloudMaintenanceMode() { /usr/bin/docker exec $(docker ps -qf name=nextcloud_cron) php /var/www/html/occ maintenance:mode --${1:-off} } function notifyAdmin() { if [ -f ${TMPDIR}/errorsOccured ]; then /usr/bin/mailx -a "From: ${HOST}.${DOMAIN} " -s "[${HOST}] ${ACTION} - errors occured" hostmaster@${DOMAIN} < ${LOGFILE} rm ${TMPDIR}/errorsOccured fi } function check() { if [ $1 -eq 1 ]; then echo '==> FAILED!' if [ ! -f ${TMPDIR}/errorsOccured ]; then touch ${TMPDIR}/errorsOccured fi return 1 fi } function dump() { while (( "$#" )); do case "${1}" in # nextcloud|all) # nextcloudMaintenanceMode on # dumpDBfromDocker postgres nextcloud # nextcloudMaintenanceMode off # ;;& wallabag|all) dumpDBfromDocker postgres wallabag ;;& # ttrss|all) # dumpDBfromDocker postgres ttrss # ;;& ejabberd|all) dumpDBfromDocker postgres ejabberd ;;& mailcow|all) while (( "$#" )); do case "${2}" in all) mailcowBaR all ;;& vmail) mailcowBaR vmail ;;& crypt) mailcowBaR crypt ;;& redis) mailcowBaR redis ;;& rspamd) mailcowBaR rspamd ;;& postfix) mailcowBaR postfix ;;& mysql) mailcowBaR mysql ;; esac shift done ;; esac shift done } function backup() { echo "###### Start backup of $HOST on $(date) ######" while (( "$#" )); do case "${2}" in rootfs|all) echo 'Get current locally installed software...' dpkg --get-selections > ${BKPPATH}/software.list check $? cat <>${TMPDIR}/fileList.borg /root /home /opt EOT cat <>${TMPDIR}/excludeList.borg /root/.ssh/rsyncnet* ${LOGPATH}/*.log ${RUNTIME_DIR} /opt/mailcow-dockerized ${DATA_DIR}/nextcloud* ${DATA_DIR}/wallabag* ${DATA_DIR}/ttrss* ${DATA_DIR}/mailcow* ${DATA_DIR}/ejabberd* ${DATA_DIR}/filer* EOT runBorgBackup rootfs ;;& etc|rootfs) cat <>${TMPDIR}/fileList.borg /etc EOT cat <>${TMPDIR}/excludeList.borg /etc/shadow* EOT runBorgBackup etc ;;& # nextcloud|all) # dump nextcloud # nextcloudMaintenanceMode on # cat <>${TMPDIR}/fileList.borg # ${CONFIG_DIR}/nextcloud # ${DATA_DIR}/nextcloud #EOT # runBorgBackup nextcloud # nextcloudMaintenanceMode off # ;;& wallabag|all) dump wallabag cat <>${TMPDIR}/fileList.borg ${CONFIG_DIR}/wallabag ${DATA_DIR}/wallabag EOT runBorgBackup wallabag ;;& # ttrss|all) # dump ttrss # cat <>${TMPDIR}/fileList.borg # ${CONFIG_DIR}/ttrss # ${DATA_DIR}/ttrss #EOT # runBorgBackup ttrss # ;;& ejabberd|all) dump ejabberd cat <>${TMPDIR}/fileList.borg ${CONFIG_DIR}/ejabberd ${DATA_DIR}/ejabberd ${CONFIG_DIR}/filer ${DATA_DIR}/filer EOT runBorgBackup ejabberd ;;& mailcow|all) # if [ "${2}" -eq 'all' ] ; then # 3='all' # fi # while (( "$#" )); do # case "${3}" in # all) dump mailcow all cat <>${TMPDIR}/fileList.borg ${ROOT_DIR}/mailcow-dockerized ${DATA_DIR}/mailcow EOT runBorgBackup mailcow # ;; # vmail) # dump mailcow vmail # # IMPLEMENT BACKUP VIA MC-VMAIL and so on!! # ;; # crypt) # dump mailcow crypt # ;; # redis) # dump mailcow redis # ;; # rspamd) # dump mailcow rspamd # ;; # postfix) # dump mailcow postfix # ;; # mysql) # dump mailcow mysql # ;; # esac # shift # done ;; esac shift done echo } function prune() { while (( "$#" )); do case "${2}" in rootfs|all) runBorgPrune rootfs ;;& etc|all) runBorgPrune rootfs ;;& # nextcloud|all) # runBorgPrune nextcloud # ;;& wallabag|all) runBorgPrune wallabag ;;& # ttrss|all) # runBorgPrune ttrss # ;;& ejabberd|all) runBorgPrune ejabberd ;;& mailcow|all) # if [ "${2}" = 'all' ] ; then # mailcowOpt='all' # fi # while (( "$#" )); do # case "${mailcowOpt}" in # all) runBorgPrune mailcow # ;; # vmail) # runBorgPrune mailcow # ;; # crypt) # runBorgPrune mailcow # ;; # redis) # runBorgPrune mailcow # ;; # rspamd) # runBorgPrune mailcow # ;; # postfix) # runBorgPrune mailcow # ;; # mysql) # runBorgPrune mailcow # ;; # esac # shift # done # ;;& # all) # pruneLogs ;; esac shift done } # Write output to logfile exec > >(tee -i ${LOGFILE}) exec 2>&1 if [[ ${ACTION} == 'dump' ]]; then dump ${@,,} elif [[ ${ACTION} == 'backup' ]]; then backup ${@,,} elif [[ ${ACTION} == 'restore' ]]; then echo 'Restore not yet implemented!' elif [[ ${ACTION} == 'prune' ]]; then prune ${@,,} elif [[ ${ACTION} == 'status' ]]; then showBorgStats ${2} fi echo "Finished!" notifyAdmin rm -r $TMPDIR