_cold-standby.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. #!/usr/bin/env bash
  2. PATH=${PATH}:/opt/bin
  3. DATE=$(date +%Y-%m-%d_%H_%M_%S)
  4. LOCAL_ARCH=$(uname -m)
  5. export LC_ALL=C
  6. echo
  7. 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."
  8. echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."
  9. echo
  10. function docker_garbage() {
  11. IMGS_TO_DELETE=()
  12. for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
  13. REPOSITORY=${container/:*}
  14. TAG=${container/*:}
  15. V_MAIN=${container/*.}
  16. V_SUB=${container/*.}
  17. EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
  18. for existing_tag in ${EXISTING_TAGS[@]}; do
  19. V_MAIN_EXISTING=${existing_tag/*.}
  20. V_SUB_EXISTING=${existing_tag/*.}
  21. # Not an integer
  22. [[ ! ${V_MAIN_EXISTING} =~ ^[0-9]+$ ]] && continue
  23. [[ ! ${V_SUB_EXISTING} =~ ^[0-9]+$ ]] && continue
  24. if [[ ${V_MAIN_EXISTING} == "latest" ]]; then
  25. echo "Found deprecated label \"latest\" for repository ${REPOSITORY}, it should be deleted."
  26. IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
  27. elif [[ ${V_MAIN_EXISTING} -lt ${V_MAIN} ]]; then
  28. echo "Found tag ${existing_tag} for ${REPOSITORY}, which is older than the current tag ${TAG} and should be deleted."
  29. IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
  30. elif [[ ${V_SUB_EXISTING} -lt ${V_SUB} ]]; then
  31. echo "Found tag ${existing_tag} for ${REPOSITORY}, which is older than the current tag ${TAG} and should be deleted."
  32. IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
  33. fi
  34. done
  35. done
  36. if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
  37. docker rmi ${IMGS_TO_DELETE[*]}
  38. fi
  39. }
  40. function preflight_local_checks() {
  41. if [[ -z "${REMOTE_SSH_KEY}" ]]; then
  42. >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
  43. exit 1
  44. fi
  45. if [[ ! -s "${REMOTE_SSH_KEY}" ]]; then
  46. >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} is empty\e[0m"
  47. exit 1
  48. fi
  49. if [[ $(stat -c "%a" "${REMOTE_SSH_KEY}") -ne 600 ]]; then
  50. >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} has insecure permissions\e[0m"
  51. exit 1
  52. fi
  53. if [[ ! -z "${REMOTE_SSH_PORT}" ]]; then
  54. if [[ ${REMOTE_SSH_PORT} != ?(-)+([0-9]) ]] || [[ ${REMOTE_SSH_PORT} -gt 65535 ]]; then
  55. >&2 echo -e "\e[31mREMOTE_SSH_PORT is set but not an integer < 65535\e[0m"
  56. exit 1
  57. fi
  58. fi
  59. if [[ -z "${REMOTE_SSH_HOST}" ]]; then
  60. >&2 echo -e "\e[31mREMOTE_SSH_HOST cannot be empty\e[0m"
  61. exit 1
  62. fi
  63. for bin in rsync docker grep cut; do
  64. if [[ -z $(which ${bin}) ]]; then
  65. >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
  66. exit 1
  67. fi
  68. done
  69. if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
  70. echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
  71. exit 1
  72. fi
  73. }
  74. function preflight_remote_checks() {
  75. if ! ssh -o StrictHostKeyChecking=no \
  76. -i "${REMOTE_SSH_KEY}" \
  77. ${REMOTE_SSH_HOST} \
  78. -p ${REMOTE_SSH_PORT} \
  79. rsync --version > /dev/null ; then
  80. >&2 echo -e "\e[31mCould not verify connection to ${REMOTE_SSH_HOST}\e[0m"
  81. >&2 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. >&2 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; 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. >&2 echo -e "\e[31mCannot find ${bin} in remote PATH, exiting...\e[0m"
  99. exit 1
  100. fi
  101. done
  102. ssh -o StrictHostKeyChecking=no \
  103. -i "${REMOTE_SSH_KEY}" \
  104. ${REMOTE_SSH_HOST} \
  105. -p ${REMOTE_SSH_PORT} \
  106. "bash -s" << "EOF"
  107. if docker compose > /dev/null 2>&1; then
  108. exit 0
  109. elif docker-compose version --short | grep "^2." > /dev/null 2>&1; then
  110. exit 1
  111. else
  112. exit 2
  113. fi
  114. EOF
  115. if [ $? = 0 ]; then
  116. COMPOSE_COMMAND="docker compose"
  117. echo "DEBUG: Using native docker compose on remote"
  118. elif [ $? = 1 ]; then
  119. COMPOSE_COMMAND="docker-compose"
  120. echo "DEBUG: Using standalone docker compose on remote"
  121. else
  122. echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
  123. exit 1
  124. fi
  125. REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m")
  126. }
  127. SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
  128. source "${SCRIPT_DIR}/../mailcow.conf"
  129. COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
  130. CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
  131. SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
  132. preflight_local_checks
  133. preflight_remote_checks
  134. echo
  135. echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
  136. echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
  137. echo
  138. # Print Message if Local Arch and Remote Arch is not the same
  139. if [[ $LOCAL_ARCH != $REMOTE_ARCH ]]; then
  140. echo
  141. echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
  142. echo -e "\e[3;33mDetected Architecture missmatch from source to destination...\e[0m"
  143. echo -e "\e[3;33mYour backup is transferred but some volumes might be skipped!\e[0m"
  144. echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
  145. echo
  146. sleep 2
  147. fi
  148. # Make sure destination exists, rsync can fail under some circumstances
  149. echo -e "\033[1mPreparing remote...\033[0m"
  150. if ! ssh -o StrictHostKeyChecking=no \
  151. -i "${REMOTE_SSH_KEY}" \
  152. ${REMOTE_SSH_HOST} \
  153. -p ${REMOTE_SSH_PORT} \
  154. mkdir -p "${SCRIPT_DIR}/../" ; then
  155. >&2 echo -e "\e[31m[ERR]\e[0m - Could not prepare remote for mailcow base directory transfer"
  156. exit 1
  157. fi
  158. # Syncing the mailcow base directory
  159. echo -e "\033[1mSynchronizing mailcow base directory...\033[0m"
  160. rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \
  161. -i \"${REMOTE_SSH_KEY}\" \
  162. -p ${REMOTE_SSH_PORT}" \
  163. "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../"
  164. ec=$?
  165. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  166. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer mailcow base directory to remote"
  167. exit 1
  168. fi
  169. # Let the remote side create all network, volumes and containers to prevent need for external:true #
  170. echo -e "\e[33mCreating networks, volumes and containers on remote...\e[0m"
  171. if ! ssh -o StrictHostKeyChecking=no \
  172. -i "${REMOTE_SSH_KEY}" \
  173. ${REMOTE_SSH_HOST} \
  174. -p ${REMOTE_SSH_PORT} \
  175. ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" create 2>&1 ; then
  176. >&2 echo -e "\e[31m[ERR]\e[0m - Could not create networks, volumes and containers on remote"
  177. fi
  178. # Trigger a Redis save for a consistent Redis copy
  179. echo -ne "\033[1mRunning redis-cli save... \033[0m"
  180. docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save
  181. # Syncing volumes related to compose project
  182. # Same here: make sure destination exists
  183. for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do
  184. mountpoint="$(docker inspect ${vol} | grep Mountpoint | cut -d '"' -f4)"
  185. echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m"
  186. ssh -o StrictHostKeyChecking=no \
  187. -i "${REMOTE_SSH_KEY}" \
  188. ${REMOTE_SSH_HOST} \
  189. -p ${REMOTE_SSH_PORT} \
  190. mkdir -p "${mountpoint}"
  191. if [[ "${vol}" =~ "mysql-vol-1" ]]; then
  192. # Make sure a previous backup does not exist
  193. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  194. echo -e "\033[1mCreating consistent backup of MariaDB volume...\033[0m"
  195. if ! docker run --rm \
  196. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  197. -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \
  198. --entrypoint= \
  199. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  200. ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then
  201. >&2 echo -e "\e[31m[ERR]\e[0m - Could not create MariaDB backup on source"
  202. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  203. exit 1
  204. fi
  205. if ! docker run --rm \
  206. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  207. --entrypoint= \
  208. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  209. ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then
  210. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"
  211. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  212. exit 1
  213. fi
  214. chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup"
  215. echo -e "\033[1mSynchronizing MariaDB backup...\033[0m"
  216. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  217. -i \"${REMOTE_SSH_KEY}\" \
  218. -p ${REMOTE_SSH_PORT}" \
  219. "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  220. ec=$?
  221. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  222. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"
  223. exit 1
  224. fi
  225. # Cleanup
  226. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  227. elif [[ "${vol}" =~ "rspamd-vol-1" ]]; then
  228. # Exclude rspamd-vol-1 if the Architectures are not the same on source and destination due to compatibility issues.
  229. if [[ $LOCAL_ARCH == $REMOTE_ARCH ]]; then
  230. echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
  231. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  232. -i \"${REMOTE_SSH_KEY}\" \
  233. -p ${REMOTE_SSH_PORT}" \
  234. "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  235. else
  236. echo -e "\e[1;31mSkipping ${vol} from local maschine due to incompatiblity between different architecture...\e[0m"
  237. sleep 2
  238. continue
  239. fi
  240. else
  241. echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
  242. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  243. -i \"${REMOTE_SSH_KEY}\" \
  244. -p ${REMOTE_SSH_PORT}" \
  245. "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  246. ec=$?
  247. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  248. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer ${vol} from local ${mountpoint} to remote"
  249. exit 1
  250. fi
  251. fi
  252. echo -e "\e[32mCompleted\e[0m"
  253. done
  254. # Restart Dockerd on destination
  255. echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"
  256. if ! ssh -o StrictHostKeyChecking=no \
  257. -i "${REMOTE_SSH_KEY}" \
  258. ${REMOTE_SSH_HOST} \
  259. -p ${REMOTE_SSH_PORT} \
  260. systemctl restart docker ; then
  261. >&2 echo -e "\e[31m[ERR]\e[0m - Could not restart Docker daemon on remote"
  262. exit 1
  263. fi
  264. echo "OK"
  265. echo -e "\e[33mPulling images on remote...\e[0m"
  266. echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m"
  267. if ! ssh -o StrictHostKeyChecking=no \
  268. -i "${REMOTE_SSH_KEY}" \
  269. ${REMOTE_SSH_HOST} \
  270. -p ${REMOTE_SSH_PORT} \
  271. ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
  272. >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
  273. fi
  274. echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m"
  275. if ! ssh -o StrictHostKeyChecking=no \
  276. -i "${REMOTE_SSH_KEY}" \
  277. ${REMOTE_SSH_HOST} \
  278. -p ${REMOTE_SSH_PORT} \
  279. ${SCRIPT_DIR}/../update.sh -f --gc ; then
  280. >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
  281. fi
  282. echo -e "\e[32mDone\e[0m"