Matthias Andreas Benkard | b382b10 | 2021-01-02 15:32:21 +0100 | [diff] [blame] | 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | DEBIAN_DOCKER_IMAGE="debian:buster-slim" |
| 4 | |
| 5 | if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then |
| 6 | BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" |
| 7 | fi |
| 8 | |
| 9 | if [[ ! ${1} =~ (backup|restore) ]]; then |
| 10 | echo "First parameter needs to be 'backup' or 'restore'" |
| 11 | exit 1 |
| 12 | fi |
| 13 | |
| 14 | if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then |
| 15 | echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql', 'all' or '--delete-days'" |
| 16 | exit 1 |
| 17 | fi |
| 18 | |
| 19 | if [[ -z ${BACKUP_LOCATION} ]]; then |
| 20 | while [[ -z ${BACKUP_LOCATION} ]]; do |
| 21 | read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION |
| 22 | done |
| 23 | fi |
| 24 | |
| 25 | if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then |
| 26 | echo "Backup directory needs to be given as absolute path (starting with /)." |
| 27 | exit 1 |
| 28 | fi |
| 29 | |
| 30 | if [[ -f ${BACKUP_LOCATION} ]]; then |
| 31 | echo "${BACKUP_LOCATION} is a file!" |
| 32 | exit 1 |
| 33 | fi |
| 34 | |
| 35 | if [[ ! -d ${BACKUP_LOCATION} ]]; then |
| 36 | echo "${BACKUP_LOCATION} is not a directory" |
| 37 | read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION |
| 38 | if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then |
| 39 | exit 1 |
| 40 | else |
| 41 | mkdir -p ${BACKUP_LOCATION} |
| 42 | chmod 755 ${BACKUP_LOCATION} |
| 43 | fi |
| 44 | else |
| 45 | if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then |
| 46 | echo "${BACKUP_LOCATION} is not write-able for others, that's required for a backup." |
| 47 | exit 1 |
| 48 | fi |
| 49 | fi |
| 50 | |
| 51 | BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##') |
| 52 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |
| 53 | COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml |
| 54 | ENV_FILE=${SCRIPT_DIR}/../.env |
| 55 | |
| 56 | if [ ! -f ${COMPOSE_FILE} ]; then |
| 57 | echo "Compose file not found" |
| 58 | exit 1 |
| 59 | fi |
| 60 | |
| 61 | if [ ! -f ${ENV_FILE} ]; then |
| 62 | echo "Environment file not found" |
| 63 | exit 1 |
| 64 | fi |
| 65 | |
| 66 | echo "Using ${BACKUP_LOCATION} as backup/restore location." |
| 67 | echo |
| 68 | |
| 69 | source ${SCRIPT_DIR}/../mailcow.conf |
| 70 | |
| 71 | if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then |
| 72 | echo "Could not determine compose project name" |
| 73 | exit 1 |
| 74 | else |
| 75 | echo "Found project name ${COMPOSE_PROJECT_NAME}" |
| 76 | CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") |
| 77 | fi |
| 78 | |
| 79 | function backup() { |
| 80 | DATE=$(date +"%Y-%m-%d-%H-%M-%S") |
| 81 | mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" |
| 82 | chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" |
| 83 | cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" |
| 84 | while (( "$#" )); do |
| 85 | case "$1" in |
| 86 | vmail|all) |
| 87 | docker run --name mailcow-backup --rm \ |
| 88 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 89 | -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:ro,z \ |
| 90 | ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_vmail.tar.gz /vmail |
| 91 | ;;& |
| 92 | crypt|all) |
| 93 | docker run --name mailcow-backup --rm \ |
| 94 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 95 | -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:ro,z \ |
| 96 | ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_crypt.tar.gz /crypt |
| 97 | ;;& |
| 98 | redis|all) |
| 99 | docker exec $(docker ps -qf name=redis-mailcow) redis-cli save |
| 100 | docker run --name mailcow-backup --rm \ |
| 101 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 102 | -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:ro,z \ |
| 103 | ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_redis.tar.gz /redis |
| 104 | ;;& |
| 105 | rspamd|all) |
| 106 | docker run --name mailcow-backup --rm \ |
| 107 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 108 | -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:ro,z \ |
| 109 | ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd |
| 110 | ;;& |
| 111 | postfix|all) |
| 112 | docker run --name mailcow-backup --rm \ |
| 113 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 114 | -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:ro,z \ |
| 115 | ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_postfix.tar.gz /postfix |
| 116 | ;;& |
| 117 | mysql|all) |
| 118 | SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) |
| 119 | if [[ -z "${SQLIMAGE}" ]]; then |
| 120 | echo "Could not determine SQL image version, skipping backup..." |
| 121 | shift |
| 122 | continue |
| 123 | else |
| 124 | echo "Using SQL image ${SQLIMAGE}, starting..." |
| 125 | docker run --name mailcow-backup --rm \ |
| 126 | --network $(docker network ls -qf name=${CMPS_PRJ}_mailcow-network) \ |
| 127 | -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro,z \ |
| 128 | --entrypoint= \ |
| 129 | --sysctl net.ipv6.conf.all.disable_ipv6=1 \ |
| 130 | -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ |
| 131 | ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \ |
| 132 | mariabackup --prepare --target-dir=/backup_mariadb ; \ |
| 133 | chown -R 999:999 /backup_mariadb ; \ |
| 134 | /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;" |
| 135 | fi |
| 136 | ;;& |
| 137 | --delete-days) |
| 138 | shift |
| 139 | if [[ "${1}" =~ ^[0-9]+$ ]]; then |
| 140 | find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; |
| 141 | else |
| 142 | echo "Parameter of --delete-days is not a number." |
| 143 | fi |
| 144 | ;; |
| 145 | esac |
| 146 | shift |
| 147 | done |
| 148 | } |
| 149 | |
| 150 | function restore() { |
| 151 | echo |
| 152 | echo "Stopping watchdog-mailcow..." |
| 153 | docker stop $(docker ps -qf name=watchdog-mailcow) |
| 154 | echo |
| 155 | RESTORE_LOCATION="${1}" |
| 156 | shift |
| 157 | while (( "$#" )); do |
| 158 | case "$1" in |
| 159 | vmail) |
| 160 | docker stop $(docker ps -qf name=dovecot-mailcow) |
| 161 | docker run -it --name mailcow-backup --rm \ |
| 162 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 163 | -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:z \ |
| 164 | ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_vmail.tar.gz |
| 165 | docker start $(docker ps -aqf name=dovecot-mailcow) |
| 166 | echo |
| 167 | echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" |
| 168 | echo |
| 169 | echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'" |
| 170 | echo |
| 171 | read -p "Force a resync now? [y|N] " FORCE_RESYNC |
| 172 | if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then |
| 173 | docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' |
| 174 | else |
| 175 | echo "OK, skipped." |
| 176 | fi |
| 177 | ;; |
| 178 | redis) |
| 179 | docker stop $(docker ps -qf name=redis-mailcow) |
| 180 | docker run -it --name mailcow-backup --rm \ |
| 181 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 182 | -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:z \ |
| 183 | ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_redis.tar.gz |
| 184 | docker start $(docker ps -aqf name=redis-mailcow) |
| 185 | ;; |
| 186 | crypt) |
| 187 | docker stop $(docker ps -qf name=dovecot-mailcow) |
| 188 | docker run -it --name mailcow-backup --rm \ |
| 189 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 190 | -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:z \ |
| 191 | ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_crypt.tar.gz |
| 192 | docker start $(docker ps -aqf name=dovecot-mailcow) |
| 193 | ;; |
| 194 | rspamd) |
| 195 | docker stop $(docker ps -qf name=rspamd-mailcow) |
| 196 | docker run -it --name mailcow-backup --rm \ |
| 197 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 198 | -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:z \ |
| 199 | ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_rspamd.tar.gz |
| 200 | docker start $(docker ps -aqf name=rspamd-mailcow) |
| 201 | ;; |
| 202 | postfix) |
| 203 | docker stop $(docker ps -qf name=postfix-mailcow) |
| 204 | docker run -it --name mailcow-backup --rm \ |
| 205 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 206 | -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:z \ |
| 207 | ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_postfix.tar.gz |
| 208 | docker start $(docker ps -aqf name=postfix-mailcow) |
| 209 | ;; |
| 210 | mysql|mariadb) |
| 211 | SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) |
| 212 | if [[ -z "${SQLIMAGE}" ]]; then |
| 213 | echo "Could not determine SQL image version, skipping restore..." |
| 214 | shift |
| 215 | continue |
| 216 | elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then |
| 217 | echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore." |
| 218 | echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." |
| 219 | shift |
| 220 | continue |
| 221 | else |
| 222 | read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW |
| 223 | if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then |
| 224 | echo "OK, skipped." |
| 225 | shift |
| 226 | continue |
| 227 | else |
| 228 | echo "Stopping mailcow..." |
| 229 | docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down |
| 230 | fi |
| 231 | #docker stop $(docker ps -qf name=mysql-mailcow) |
| 232 | if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then |
| 233 | docker run --name mailcow-backup --rm \ |
| 234 | -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:rw,z \ |
| 235 | --entrypoint= \ |
| 236 | -v ${RESTORE_LOCATION}/mysql:/backup:z \ |
| 237 | ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" |
| 238 | elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then |
| 239 | docker run \ |
| 240 | -it --name mailcow-backup --rm \ |
| 241 | -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:z \ |
| 242 | --entrypoint= \ |
| 243 | -u mysql \ |
| 244 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 245 | ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \ |
| 246 | until mysqladmin ping; do sleep 3; done && \ |
| 247 | echo Restoring... && \ |
| 248 | gunzip < backup/backup_mysql.gz | mysql -uroot && \ |
| 249 | mysql -uroot -e SHUTDOWN;" |
| 250 | elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then |
| 251 | docker run --name mailcow-backup --rm \ |
| 252 | -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/backup_mariadb/:rw,z \ |
| 253 | --entrypoint= \ |
| 254 | -v ${RESTORE_LOCATION}:/backup:z \ |
| 255 | ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ |
| 256 | /bin/rm -rf /backup_mariadb/* ; \ |
| 257 | /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" |
| 258 | fi |
| 259 | echo "Modifying mailcow.conf..." |
| 260 | source ${RESTORE_LOCATION}/mailcow.conf |
| 261 | sed -i --follow-symlinks "/DBNAME/c\DBNAME=${DBNAME}" ${SCRIPT_DIR}/../mailcow.conf |
| 262 | sed -i --follow-symlinks "/DBUSER/c\DBUSER=${DBUSER}" ${SCRIPT_DIR}/../mailcow.conf |
| 263 | sed -i --follow-symlinks "/DBPASS/c\DBPASS=${DBPASS}" ${SCRIPT_DIR}/../mailcow.conf |
| 264 | sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf |
| 265 | source ${SCRIPT_DIR}/../mailcow.conf |
| 266 | echo "Starting mailcow..." |
| 267 | docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d |
| 268 | #docker start $(docker ps -aqf name=mysql-mailcow) |
| 269 | fi |
| 270 | ;; |
| 271 | esac |
| 272 | shift |
| 273 | done |
| 274 | echo |
| 275 | echo "Starting watchdog-mailcow..." |
| 276 | docker start $(docker ps -aqf name=watchdog-mailcow) |
| 277 | } |
| 278 | |
| 279 | if [[ ${1} == "backup" ]]; then |
| 280 | backup ${@,,} |
| 281 | elif [[ ${1} == "restore" ]]; then |
| 282 | i=1 |
| 283 | declare -A FOLDER_SELECTION |
| 284 | if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then |
| 285 | echo "Selected backup location has no subfolders" |
| 286 | exit 1 |
| 287 | fi |
| 288 | for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do |
| 289 | echo "[ ${i} ] - ${folder}" |
| 290 | FOLDER_SELECTION[${i}]="${folder}" |
| 291 | ((i++)) |
| 292 | done |
| 293 | echo |
| 294 | input_sel=0 |
| 295 | while [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do |
| 296 | read -p "Select a restore point: " input_sel |
| 297 | done |
| 298 | i=1 |
| 299 | echo |
| 300 | declare -A FILE_SELECTION |
| 301 | RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" |
| 302 | if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then |
| 303 | echo "No datasets found" |
| 304 | exit 1 |
| 305 | fi |
| 306 | |
| 307 | echo "[ 0 ] - all" |
| 308 | # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz |
| 309 | FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') |
| 310 | for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do |
| 311 | if [[ ${file} =~ vmail ]]; then |
| 312 | echo "[ ${i} ] - Mail directory (/var/vmail)" |
| 313 | FILE_SELECTION[${i}]="vmail" |
| 314 | ((i++)) |
| 315 | elif [[ ${file} =~ crypt ]]; then |
| 316 | echo "[ ${i} ] - Crypt data" |
| 317 | FILE_SELECTION[${i}]="crypt" |
| 318 | ((i++)) |
| 319 | elif [[ ${file} =~ redis ]]; then |
| 320 | echo "[ ${i} ] - Redis DB" |
| 321 | FILE_SELECTION[${i}]="redis" |
| 322 | ((i++)) |
| 323 | elif [[ ${file} =~ rspamd ]]; then |
| 324 | echo "[ ${i} ] - Rspamd data" |
| 325 | FILE_SELECTION[${i}]="rspamd" |
| 326 | ((i++)) |
| 327 | elif [[ ${file} =~ postfix ]]; then |
| 328 | echo "[ ${i} ] - Postfix data" |
| 329 | FILE_SELECTION[${i}]="postfix" |
| 330 | ((i++)) |
| 331 | elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then |
| 332 | echo "[ ${i} ] - SQL DB" |
| 333 | FILE_SELECTION[${i}]="mysql" |
| 334 | ((i++)) |
| 335 | fi |
| 336 | done |
| 337 | echo |
| 338 | input_sel=-1 |
| 339 | while [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do |
| 340 | read -p "Select a dataset to restore: " input_sel |
| 341 | done |
| 342 | echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." |
| 343 | restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} |
| 344 | fi |