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