backup_and_restore.sh 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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) ]]; then
  10. echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql' or 'all'"
  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. echo "Using ${BACKUP_LOCATION} as backup/restore location."
  45. echo
  46. source ${SCRIPT_DIR}/../mailcow.conf
  47. CMPS_PRJ=$(echo $COMPOSE_PROJECT_NAME | tr -cd "[A-Za-z-_]")
  48. function backup() {
  49. DATE=$(date +"%Y-%m-%d-%H-%M-%S")
  50. mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
  51. chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
  52. cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
  53. while (( "$#" )); do
  54. case "$1" in
  55. vmail|all)
  56. docker run --rm \
  57. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  58. -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:ro \
  59. debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_vmail.tar.gz /vmail
  60. ;;&
  61. crypt|all)
  62. docker run --rm \
  63. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  64. -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:ro \
  65. debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_crypt.tar.gz /crypt
  66. ;;&
  67. redis|all)
  68. docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
  69. docker run --rm \
  70. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  71. -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:ro \
  72. debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_redis.tar.gz /redis
  73. ;;&
  74. rspamd|all)
  75. docker run --rm \
  76. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  77. -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:ro \
  78. debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd
  79. ;;&
  80. postfix|all)
  81. docker run --rm \
  82. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  83. -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:ro \
  84. debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_postfix.tar.gz /postfix
  85. ;;&
  86. mysql|all)
  87. SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
  88. docker run --rm \
  89. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  90. -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \
  91. --entrypoint= \
  92. -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
  93. ${SQLIMAGE} /bin/sh -c "mysqldump -hmysql -uroot -p${DBROOT} --all-databases | gzip > /backup/backup_mysql.gz"
  94. ;;
  95. esac
  96. shift
  97. done
  98. }
  99. function restore() {
  100. docker stop $(docker ps -qf name=watchdog-mailcow)
  101. RESTORE_LOCATION="${1}"
  102. shift
  103. while (( "$#" )); do
  104. case "$1" in
  105. vmail)
  106. docker stop $(docker ps -qf name=dovecot-mailcow)
  107. docker run -it --rm \
  108. -v ${RESTORE_LOCATION}:/backup \
  109. -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail \
  110. debian:stretch-slim /bin/tar -Pxvzf /backup/backup_vmail.tar.gz
  111. docker start $(docker ps -aqf name=dovecot-mailcow)
  112. echo
  113. 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:"
  114. echo
  115. echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'"
  116. echo
  117. read -p "Force a resync now? [y|N] " FORCE_RESYNC
  118. if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then
  119. docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'
  120. else
  121. echo "OK, skipped."
  122. fi
  123. ;;
  124. redis)
  125. docker stop $(docker ps -qf name=redis-mailcow)
  126. docker run -it --rm \
  127. -v ${RESTORE_LOCATION}:/backup \
  128. -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis \
  129. debian:stretch-slim /bin/tar -Pxvzf /backup/backup_redis.tar.gz
  130. docker start $(docker ps -aqf name=redis-mailcow)
  131. ;;
  132. crypt)
  133. docker stop $(docker ps -qf name=dovecot-mailcow)
  134. docker run -it --rm \
  135. -v ${RESTORE_LOCATION}:/backup \
  136. -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt \
  137. debian:stretch-slim /bin/tar -Pxvzf /backup/backup_crypt.tar.gz
  138. docker start $(docker ps -aqf name=dovecot-mailcow)
  139. ;;
  140. rspamd)
  141. docker stop $(docker ps -qf name=rspamd-mailcow)
  142. docker run -it --rm \
  143. -v ${RESTORE_LOCATION}:/backup \
  144. -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd \
  145. debian:stretch-slim /bin/tar -Pxvzf /backup/backup_rspamd.tar.gz
  146. docker start $(docker ps -aqf name=rspamd-mailcow)
  147. ;;
  148. postfix)
  149. docker stop $(docker ps -qf name=postfix-mailcow)
  150. docker run -it --rm \
  151. -v ${RESTORE_LOCATION}:/backup \
  152. -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix \
  153. debian:stretch-slim /bin/tar -Pxvzf /backup/backup_postfix.tar.gz
  154. docker start $(docker ps -aqf name=postfix-mailcow)
  155. ;;
  156. mysql)
  157. SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
  158. docker stop $(docker ps -qf name=mysql-mailcow)
  159. docker run \
  160. -it --rm \
  161. -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/ \
  162. --entrypoint= \
  163. -u mysql \
  164. -v ${RESTORE_LOCATION}:/backup \
  165. ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \
  166. until mysqladmin ping; do sleep 3; done && \
  167. echo Restoring... && \
  168. gunzip < backup/backup_mysql.gz | mysql -uroot && \
  169. mysql -uroot -e SHUTDOWN;"
  170. docker start $(docker ps -aqf name=mysql-mailcow)
  171. ;;
  172. esac
  173. shift
  174. done
  175. docker start $(docker ps -aqf name=watchdog-mailcow)
  176. }
  177. if [[ ${1} == "backup" ]]; then
  178. backup ${@,,}
  179. elif [[ ${1} == "restore" ]]; then
  180. i=1
  181. declare -A FOLDER_SELECTION
  182. if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then
  183. echo "Selected backup location has no subfolders"
  184. exit 1
  185. fi
  186. for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do
  187. echo "[ ${i} ] - ${folder}"
  188. FOLDER_SELECTION[${i}]="${folder}"
  189. ((i++))
  190. done
  191. echo
  192. input_sel=0
  193. while [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do
  194. read -p "Select a restore point: " input_sel
  195. done
  196. i=1
  197. echo
  198. declare -A FILE_SELECTION
  199. RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}"
  200. if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 -type f -regex ".*\(redis\|rspamd\|mysql\|crypt\|vmail\|postfix\).*") ]]; then
  201. echo "No datasets found"
  202. exit 1
  203. fi
  204. for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do
  205. if [[ ${file} =~ vmail ]]; then
  206. echo "[ ${i} ] - Mail directory (/var/vmail)"
  207. FILE_SELECTION[${i}]="vmail"
  208. ((i++))
  209. elif [[ ${file} =~ crypt ]]; then
  210. echo "[ ${i} ] - Crypt data"
  211. FILE_SELECTION[${i}]="crypt"
  212. ((i++))
  213. elif [[ ${file} =~ redis ]]; then
  214. echo "[ ${i} ] - Redis DB"
  215. FILE_SELECTION[${i}]="redis"
  216. ((i++))
  217. elif [[ ${file} =~ rspamd ]]; then
  218. echo "[ ${i} ] - Rspamd data"
  219. FILE_SELECTION[${i}]="rspamd"
  220. ((i++))
  221. elif [[ ${file} =~ postfix ]]; then
  222. echo "[ ${i} ] - Postfix data"
  223. FILE_SELECTION[${i}]="postfix"
  224. ((i++))
  225. elif [[ ${file} =~ mysql ]]; then
  226. echo "[ ${i} ] - SQL DB"
  227. FILE_SELECTION[${i}]="mysql"
  228. ((i++))
  229. fi
  230. done
  231. echo
  232. input_sel=0
  233. while [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do
  234. read -p "Select a dataset to restore: " input_sel
  235. done
  236. echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..."
  237. restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]}
  238. fi