_cold-standby.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
  42. exit 1
  43. fi
  44. if [[ ! -s "${REMOTE_SSH_KEY}" ]]; then
  45. >&2 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. >&2 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. >&2 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. >&2 echo -e "\e[31mREMOTE_SSH_HOST cannot be empty\e[0m"
  60. exit 1
  61. fi
  62. for bin in rsync docker grep cut; do
  63. if [[ -z $(which ${bin}) ]]; then
  64. >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
  65. exit 1
  66. fi
  67. done
  68. echo "checking docker compose version...";
  69. if docker --help | grep compose
  70. then
  71. echo ''
  72. elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1
  73. then
  74. >&2 echo -e "\e[31mWARN: Your machine is using Docker-Compose v1!\e[0m"
  75. >&2 echo -e "\e[31mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
  76. >&2 echo -e "\e[31mPlease consider a upgrade to Docker-Compose v2.\e[0m"
  77. >&2 echo
  78. >&2 echo
  79. >&2 echo -e "\e[33mContinuing...\e[0m"
  80. sleep 3
  81. else
  82. >&2 echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m"
  83. exit 1
  84. fi
  85. if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
  86. >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
  87. exit 1
  88. fi
  89. }
  90. function preflight_remote_checks() {
  91. if ! ssh -o StrictHostKeyChecking=no \
  92. -i "${REMOTE_SSH_KEY}" \
  93. ${REMOTE_SSH_HOST} \
  94. -p ${REMOTE_SSH_PORT} \
  95. rsync --version > /dev/null ; then
  96. >&2 echo -e "\e[31mCould not verify connection to ${REMOTE_SSH_HOST}\e[0m"
  97. >&2 echo -e "\e[31mPlease check the output above (is rsync >= 3.1.0 installed on the remote system?)\e[0m"
  98. exit 1
  99. fi
  100. if ssh -o StrictHostKeyChecking=no \
  101. -i "${REMOTE_SSH_KEY}" \
  102. ${REMOTE_SSH_HOST} \
  103. -p ${REMOTE_SSH_PORT} \
  104. grep --help 2>&1 | head -n 1 | grep -q -i "busybox" ; then
  105. >&2 echo -e "\e[31mBusyBox grep detected on remote system ${REMOTE_SSH_HOST}, please install GNU grep\e[0m"
  106. exit 1
  107. fi
  108. for bin in rsync docker; do
  109. if ! ssh -o StrictHostKeyChecking=no \
  110. -i "${REMOTE_SSH_KEY}" \
  111. ${REMOTE_SSH_HOST} \
  112. -p ${REMOTE_SSH_PORT} \
  113. which ${bin} > /dev/null ; then
  114. >&2 echo -e "\e[31mCannot find ${bin} in remote PATH, exiting...\e[0m"
  115. exit 1
  116. fi
  117. done
  118. echo "checking docker compose version on remote...";
  119. if ssh -q -o StrictHostKeyChecking=no \
  120. -i "${REMOTE_SSH_KEY}" \
  121. ${REMOTE_SSH_HOST} \
  122. -p ${REMOTE_SSH_PORT} \
  123. -t docker --help | grep compose
  124. then
  125. COMPOSE_COMMAND="docker compose"
  126. elif ssh -q -o StrictHostKeyChecking=no \
  127. -i "${REMOTE_SSH_KEY}" \
  128. ${REMOTE_SSH_HOST} \
  129. -p ${REMOTE_SSH_PORT} \
  130. 'docker-compose version --short' | grep -m1 "^1" > /dev/null 2>&1
  131. then
  132. >&2 echo -e "\e[31mWARN: The remote is using Docker-Compose v1!\e[0m"
  133. >&2 echo -e "\e[31mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
  134. >&2 echo -e "\e[31mPlease consider a upgrade to Docker-Compose v2 on remote.\e[0m"
  135. >&2 echo
  136. >&2 echo
  137. >&2 echo -e "\e[33mContinuing...\e[0m"
  138. sleep 3
  139. COMPOSE_COMMAND="docker-compose"
  140. else
  141. >&2 echo -e "\e[31mCannot find Docker-Compose v1 or v2 on the Remote Machine! Please install Docker-Compose v2 on that and re-run the script.\e[0m"
  142. exit 1
  143. fi
  144. }
  145. preflight_local_checks
  146. preflight_remote_checks
  147. SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
  148. COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
  149. source "${SCRIPT_DIR}/../mailcow.conf"
  150. CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
  151. SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
  152. echo
  153. echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
  154. echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
  155. echo
  156. # Make sure destination exists, rsync can fail under some circumstances
  157. echo -e "\033[1mPreparing remote...\033[0m"
  158. if ! ssh -o StrictHostKeyChecking=no \
  159. -i "${REMOTE_SSH_KEY}" \
  160. ${REMOTE_SSH_HOST} \
  161. -p ${REMOTE_SSH_PORT} \
  162. mkdir -p "${SCRIPT_DIR}/../" ; then
  163. >&2 echo -e "\e[31m[ERR]\e[0m - Could not prepare remote for mailcow base directory transfer"
  164. exit 1
  165. fi
  166. # Syncing the mailcow base directory
  167. echo -e "\033[1mSynchronizing mailcow base directory...\033[0m"
  168. rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \
  169. -i \"${REMOTE_SSH_KEY}\" \
  170. -p ${REMOTE_SSH_PORT}" \
  171. "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../"
  172. ec=$?
  173. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  174. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer mailcow base directory to remote"
  175. exit 1
  176. fi
  177. # Trigger a Redis save for a consistent Redis copy
  178. echo -ne "\033[1mRunning redis-cli save... \033[0m"
  179. docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
  180. # Syncing volumes related to compose project
  181. # Same here: make sure destination exists
  182. for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do
  183. mountpoint="$(docker inspect ${vol} | grep Mountpoint | cut -d '"' -f4)"
  184. echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m"
  185. ssh -o StrictHostKeyChecking=no \
  186. -i "${REMOTE_SSH_KEY}" \
  187. ${REMOTE_SSH_HOST} \
  188. -p ${REMOTE_SSH_PORT} \
  189. mkdir -p "${mountpoint}"
  190. if [[ "${vol}" =~ "mysql-vol-1" ]]; then
  191. # Make sure a previous backup does not exist
  192. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  193. echo -e "\033[1mCreating consistent backup of MariaDB volume...\033[0m"
  194. if ! docker run --rm \
  195. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  196. -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \
  197. --entrypoint= \
  198. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  199. ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then
  200. >&2 echo -e "\e[31m[ERR]\e[0m - Could not create MariaDB backup on source"
  201. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  202. exit 1
  203. fi
  204. if ! docker run --rm \
  205. --network $(docker network ls -qf name=${CMPS_PRJ}_) \
  206. --entrypoint= \
  207. -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \
  208. ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then
  209. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"
  210. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  211. exit 1
  212. fi
  213. chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup"
  214. echo -e "\033[1mSynchronizing MariaDB backup...\033[0m"
  215. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  216. -i \"${REMOTE_SSH_KEY}\" \
  217. -p ${REMOTE_SSH_PORT}" \
  218. "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  219. ec=$?
  220. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  221. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"
  222. exit 1
  223. fi
  224. # Cleanup
  225. rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
  226. else
  227. echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
  228. rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
  229. -i \"${REMOTE_SSH_KEY}\" \
  230. -p ${REMOTE_SSH_PORT}" \
  231. "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
  232. ec=$?
  233. if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then
  234. >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer ${vol} from local ${mountpoint} to remote"
  235. exit 1
  236. fi
  237. fi
  238. echo -e "\e[32mCompleted\e[0m"
  239. done
  240. # Restart Dockerd on destination
  241. echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"
  242. if ! ssh -o StrictHostKeyChecking=no \
  243. -i "${REMOTE_SSH_KEY}" \
  244. ${REMOTE_SSH_HOST} \
  245. -p ${REMOTE_SSH_PORT} \
  246. systemctl restart docker ; then
  247. >&2 echo -e "\e[31m[ERR]\e[0m - Could not restart Docker daemon on remote"
  248. exit 1
  249. fi
  250. echo "OK"
  251. echo -e "\e[33mPulling images on remote...\e[0m"
  252. echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m"
  253. if ! ssh -o StrictHostKeyChecking=no \
  254. -i "${REMOTE_SSH_KEY}" \
  255. ${REMOTE_SSH_HOST} \
  256. -p ${REMOTE_SSH_PORT} \
  257. $COMPOSE_COMMAND -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
  258. >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
  259. fi
  260. echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m"
  261. if ! ssh -o StrictHostKeyChecking=no \
  262. -i "${REMOTE_SSH_KEY}" \
  263. ${REMOTE_SSH_HOST} \
  264. -p ${REMOTE_SSH_PORT} \
  265. ${SCRIPT_DIR}/../update.sh -f --gc ; then
  266. >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
  267. fi
  268. echo -e "\e[32mDone\e[0m"