_cold-standby.sh 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. #!/usr/bin/env bash
  2. PATH=$PATH:/opt/bin
  3. DATE=$(date +%Y-%m-%d_%H_%M_%S)
  4. export LC_ALL=C
  5. echo
  6. echo "If this script is run automatically by cron or a timer AND you are using block-level snapshots on your backup destination, make sure both do not run at the same time."
  7. echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."
  8. echo
  9. function docker_garbage() {
  10. IMGS_TO_DELETE=()
  11. for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
  12. REPOSITORY=${container/:*}
  13. TAG=${container/*:}
  14. V_MAIN=${container/*.}
  15. V_SUB=${container/*.}
  16. EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
  17. for existing_tag in ${EXISTING_TAGS[@]}; do
  18. V_MAIN_EXISTING=${existing_tag/*.}
  19. V_SUB_EXISTING=${existing_tag/*.}
  20. # Not an integer
  21. [[ ! $V_MAIN_EXISTING =~ ^[0-9]+$ ]] && continue
  22. [[ ! $V_SUB_EXISTING =~ ^[0-9]+$ ]] && continue
  23. if [[ $V_MAIN_EXISTING == "latest" ]]; then
  24. echo "Found deprecated label \"latest\" for repository $REPOSITORY, it should be deleted."
  25. IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
  26. elif [[ $V_MAIN_EXISTING -lt $V_MAIN ]]; then
  27. echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
  28. IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
  29. elif [[ $V_SUB_EXISTING -lt $V_SUB ]]; then
  30. echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
  31. IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
  32. fi
  33. done
  34. done
  35. if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
  36. docker rmi ${IMGS_TO_DELETE[*]}
  37. fi
  38. }
  39. function preflight_local_checks() {
  40. if [[ -z "${REMOTE_SSH_KEY}" ]]; then
  41. echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
  42. exit 1
  43. fi
  44. if [[ ! -s "${REMOTE_SSH_KEY}" ]]; then
  45. echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} is empty\e[0m"
  46. exit 1
  47. fi
  48. if [[ $(stat -c "%a" "${REMOTE_SSH_KEY}") -ne 600 ]]; then
  49. echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} has insecure permissions\e[0m"
  50. exit 1
  51. fi
  52. if [[ ! -z "${REMOTE_SSH_PORT}" ]]; then
  53. if [[ ${REMOTE_SSH_PORT} != ?(-)+([0-9]) ]] || [[ ${REMOTE_SSH_PORT} -gt 65535 ]]; then
  54. echo -e "\e[31mREMOTE_SSH_PORT is set but not an integer < 65535\e[0m"
  55. exit 1
  56. fi
  57. fi
  58. if [[ -z "${REMOTE_SSH_HOST}" ]]; then
  59. echo -e "\e[31mREMOTE_SSH_HOST cannot be empty\e[0m"
  60. exit 1
  61. fi
  62. for bin in rsync docker-compose docker grep cut; do
  63. if [[ -z $(which ${bin}) ]]; then
  64. echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
  65. exit 1
  66. fi
  67. done
  68. if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
  69. echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
  70. exit 1
  71. fi
  72. }
  73. function preflight_remote_checks() {
  74. ssh -o StrictHostKeyChecking=no \
  75. -i "${REMOTE_SSH_KEY}" \
  76. ${REMOTE_SSH_HOST} \
  77. -p ${REMOTE_SSH_PORT} \
  78. rsync -V > /dev/null
  79. if [ $? -ne 0 ]; then
  80. echo -e "\e[31mCould not verify connection to ${REMOTE_SSH_HOST}\e[0m"
  81. echo -e "\e[31mPlease check the output above (is rsync >= 3.1.0 installed on the remote system?)\e[0m"
  82. exit 1
  83. fi
  84. if ssh -o StrictHostKeyChecking=no \
  85. -i "${REMOTE_SSH_KEY}" \
  86. ${REMOTE_SSH_HOST} \
  87. -p ${REMOTE_SSH_PORT} \
  88. grep --help 2>&1 | head -n 1 | grep -q -i "busybox" ; then
  89. echo -e "\e[31mBusyBox grep detected on remote system ${REMOTE_SSH_HOST}, please install GNU grep\e[0m"
  90. exit 1
  91. fi
  92. for bin in rsync docker-compose docker; do
  93. if ! ssh -o StrictHostKeyChecking=no \
  94. -i "${REMOTE_SSH_KEY}" \
  95. ${REMOTE_SSH_HOST} \
  96. -p ${REMOTE_SSH_PORT} \
  97. which ${bin} > /dev/null ; then
  98. echo -e "\e[31mCannot find ${bin} in remote PATH, exiting...\e[0m"
  99. exit 1
  100. fi
  101. done
  102. }
  103. preflight_local_checks
  104. preflight_remote_checks
  105. SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
  106. COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
  107. source "${SCRIPT_DIR}/../mailcow.conf"
  108. CMPS_PRJ=$(echo $COMPOSE_PROJECT_NAME | tr -cd "[A-Za-z-_]")
  109. SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
  110. echo
  111. echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
  112. echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
  113. echo
  114. # Make sure destination exists, rsync can fail under some circumstances
  115. echo -e "\033[1mPreparing remote...\033[0m"
  116. ssh -o StrictHostKeyChecking=no \
  117. -i "${REMOTE_SSH_KEY}" \
  118. ${REMOTE_SSH_HOST} \
  119. -p ${REMOTE_SSH_PORT} \
  120. mkdir -p "${SCRIPT_DIR}/../"
  121. # Syncing the mailcow base directory
  122. echo -e "\033[1mSynchronizing mailcow base directory...\033[0m"
  123. rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \
  124. -i \"${REMOTE_SSH_KEY}\" \
  125. -p ${REMOTE_SSH_PORT}" \
  126. "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../"
  127. # Trigger a Redis save for a consistent Redis copy
  128. echo -ne "\033[1mRunning redis-cli save... \033[0m"
  129. docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
  130. # Syncing volumes related to compose project
  131. # Same here: make sure destination exists
  132. for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do
  133. mountpoint="$(docker inspect $vol | grep Mountpoint | cut -d '"' -f4)"
  134. echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m"
  135. ssh -o StrictHostKeyChecking=no \
  136. -i "${REMOTE_SSH_KEY}" \
  137. ${REMOTE_SSH_HOST} \
  138. -p ${REMOTE_SSH_PORT} \
  139. mkdir -p "${mountpoint}"
  140. if [[ "${vol}" =~ "mysql-vol-1" ]]; then
  141. # Make sure a previous backup does not exist
  142. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  143. echo -e "\033[1mCreating consistent backup for MariaDB volume...\033[0m"
  144. if ! docker run --rm \
  145. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  146. -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \
  147. --entrypoint= \
  148. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  149. ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then
  150. echo -e "\e[31mFATAL: mariabackup failed, exiting\e[0m"
  151. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  152. exit 1
  153. fi
  154. if ! docker run --rm \
  155. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  156. --entrypoint= \
  157. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  158. ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then
  159. echo -e "\e[31mFATAL: mariabackup failed, exiting\e[0m"
  160. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  161. exit 1
  162. fi
  163. chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup"
  164. echo -e "\033[1mSynchronizing MariaDB backup...\033[0m"
  165. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  166. -i \"${REMOTE_SSH_KEY}\" \
  167. -p ${REMOTE_SSH_PORT}" \
  168. "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  169. # Cleanup
  170. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  171. else
  172. echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
  173. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  174. -i \"${REMOTE_SSH_KEY}\" \
  175. -p ${REMOTE_SSH_PORT}" \
  176. "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  177. fi
  178. echo -e "\e[32mCompleted\e[0m"
  179. done
  180. # Restart Dockerd on destination
  181. echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"
  182. ssh -o StrictHostKeyChecking=no \
  183. -i "${REMOTE_SSH_KEY}" \
  184. ${REMOTE_SSH_HOST} \
  185. -p ${REMOTE_SSH_PORT} \
  186. systemctl restart docker
  187. echo "OK"
  188. echo -e "\033[1mPulling images on remote...\033[0m"
  189. ssh -o StrictHostKeyChecking=no \
  190. -i "${REMOTE_SSH_KEY}" \
  191. ${REMOTE_SSH_HOST} \
  192. -p ${REMOTE_SSH_PORT} \
  193. docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel
  194. echo -e "\033[1mForcing garbage cleanup on remote...\033[0m"
  195. ssh -o StrictHostKeyChecking=no \
  196. -i "${REMOTE_SSH_KEY}" \
  197. ${REMOTE_SSH_HOST} \
  198. -p ${REMOTE_SSH_PORT} \
  199. ${SCRIPT_DIR}/../update.sh -f --gc
  200. echo -e "\e[32mDone\e[0m"