backup_and_restore.sh 12 KB

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