backup_and_restore.sh 8.8 KB

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