blob: 82002d7b0225acdab7f0cda90c66e1a2d1d84f7e [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001#!/usr/bin/env bash
2
3DEBIAN_DOCKER_IMAGE="debian:buster-slim"
4
5if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then
6 BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}"
7fi
8
9if [[ ! ${1} =~ (backup|restore) ]]; then
10 echo "First parameter needs to be 'backup' or 'restore'"
11 exit 1
12fi
13
14if [[ ${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
17fi
18
19if [[ -z ${BACKUP_LOCATION} ]]; then
20 while [[ -z ${BACKUP_LOCATION} ]]; do
21 read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION
22 done
23fi
24
25if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then
26 echo "Backup directory needs to be given as absolute path (starting with /)."
27 exit 1
28fi
29
30if [[ -f ${BACKUP_LOCATION} ]]; then
31 echo "${BACKUP_LOCATION} is a file!"
32 exit 1
33fi
34
35if [[ ! -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
44else
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
49fi
50
51BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##')
52SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
53COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml
54ENV_FILE=${SCRIPT_DIR}/../.env
55
56if [ ! -f ${COMPOSE_FILE} ]; then
57 echo "Compose file not found"
58 exit 1
59fi
60
61if [ ! -f ${ENV_FILE} ]; then
62 echo "Environment file not found"
63 exit 1
64fi
65
66echo "Using ${BACKUP_LOCATION} as backup/restore location."
67echo
68
69source ${SCRIPT_DIR}/../mailcow.conf
70
71if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then
72 echo "Could not determine compose project name"
73 exit 1
74else
75 echo "Found project name ${COMPOSE_PROJECT_NAME}"
76 CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
77fi
78
79function 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
150function 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
279if [[ ${1} == "backup" ]]; then
280 backup ${@,,}
281elif [[ ${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}]}
344fi