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 | |
| 80 | for bin in rsync docker-compose docker grep cut; do |
| 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 |
| 88 | >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" |
| 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 | |
| 114 | for bin in rsync docker-compose docker; do |
| 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 | |
| 125 | } |
| 126 | |
| 127 | preflight_local_checks |
| 128 | preflight_remote_checks |
| 129 | |
| 130 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) |
| 131 | COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml" |
| 132 | source "${SCRIPT_DIR}/../mailcow.conf" |
| 133 | CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_') |
| 134 | SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}") |
| 135 | |
| 136 | echo |
| 137 | echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m" |
| 138 | echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m" |
| 139 | echo |
| 140 | |
| 141 | # Make sure destination exists, rsync can fail under some circumstances |
| 142 | echo -e "\033[1mPreparing remote...\033[0m" |
| 143 | if ! ssh -o StrictHostKeyChecking=no \ |
| 144 | -i "${REMOTE_SSH_KEY}" \ |
| 145 | ${REMOTE_SSH_HOST} \ |
| 146 | -p ${REMOTE_SSH_PORT} \ |
| 147 | mkdir -p "${SCRIPT_DIR}/../" ; then |
| 148 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not prepare remote for mailcow base directory transfer" |
| 149 | exit 1 |
| 150 | fi |
| 151 | |
| 152 | # Syncing the mailcow base directory |
| 153 | echo -e "\033[1mSynchronizing mailcow base directory...\033[0m" |
| 154 | rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \ |
| 155 | -i \"${REMOTE_SSH_KEY}\" \ |
| 156 | -p ${REMOTE_SSH_PORT}" \ |
| 157 | "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../" |
| 158 | ec=$? |
| 159 | if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then |
| 160 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer mailcow base directory to remote" |
| 161 | exit 1 |
| 162 | fi |
| 163 | |
| 164 | # Trigger a Redis save for a consistent Redis copy |
| 165 | echo -ne "\033[1mRunning redis-cli save... \033[0m" |
| 166 | docker exec $(docker ps -qf name=redis-mailcow) redis-cli save |
| 167 | |
| 168 | # Syncing volumes related to compose project |
| 169 | # Same here: make sure destination exists |
| 170 | for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do |
| 171 | |
| 172 | mountpoint="$(docker inspect ${vol} | grep Mountpoint | cut -d '"' -f4)" |
| 173 | |
| 174 | echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m" |
| 175 | |
| 176 | ssh -o StrictHostKeyChecking=no \ |
| 177 | -i "${REMOTE_SSH_KEY}" \ |
| 178 | ${REMOTE_SSH_HOST} \ |
| 179 | -p ${REMOTE_SSH_PORT} \ |
| 180 | mkdir -p "${mountpoint}" |
| 181 | |
| 182 | if [[ "${vol}" =~ "mysql-vol-1" ]]; then |
| 183 | |
| 184 | # Make sure a previous backup does not exist |
| 185 | rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" |
| 186 | |
| 187 | echo -e "\033[1mCreating consistent backup of MariaDB volume...\033[0m" |
| 188 | if ! docker run --rm \ |
| 189 | --network $(docker network ls -qf name=${CMPS_PRJ}_) \ |
| 190 | -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \ |
| 191 | --entrypoint= \ |
| 192 | -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \ |
| 193 | ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then |
| 194 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not create MariaDB backup on source" |
| 195 | rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" |
| 196 | exit 1 |
| 197 | fi |
| 198 | |
| 199 | if ! docker run --rm \ |
| 200 | --network $(docker network ls -qf name=${CMPS_PRJ}_) \ |
| 201 | --entrypoint= \ |
| 202 | -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \ |
| 203 | ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then |
| 204 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote" |
| 205 | rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" |
| 206 | exit 1 |
| 207 | fi |
| 208 | |
| 209 | chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup" |
| 210 | |
| 211 | echo -e "\033[1mSynchronizing MariaDB backup...\033[0m" |
| 212 | rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \ |
| 213 | -i \"${REMOTE_SSH_KEY}\" \ |
| 214 | -p ${REMOTE_SSH_PORT}" \ |
| 215 | "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}" |
| 216 | ec=$? |
| 217 | if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then |
| 218 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote" |
| 219 | exit 1 |
| 220 | fi |
| 221 | |
| 222 | # Cleanup |
| 223 | rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" |
| 224 | |
| 225 | else |
| 226 | |
| 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 | |
| 239 | echo -e "\e[32mCompleted\e[0m" |
| 240 | |
| 241 | done |
| 242 | |
| 243 | # Restart Dockerd on destination |
| 244 | echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m" |
| 245 | if ! ssh -o StrictHostKeyChecking=no \ |
| 246 | -i "${REMOTE_SSH_KEY}" \ |
| 247 | ${REMOTE_SSH_HOST} \ |
| 248 | -p ${REMOTE_SSH_PORT} \ |
| 249 | systemctl restart docker ; then |
| 250 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not restart Docker daemon on remote" |
| 251 | exit 1 |
| 252 | fi |
| 253 | echo "OK" |
| 254 | |
| 255 | echo -e "\033[1mPulling images on remote...\033[0m" |
| 256 | if ! ssh -o StrictHostKeyChecking=no \ |
| 257 | -i "${REMOTE_SSH_KEY}" \ |
| 258 | ${REMOTE_SSH_HOST} \ |
| 259 | -p ${REMOTE_SSH_PORT} \ |
| 260 | docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel 2>&1 ; then |
| 261 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" |
| 262 | fi |
| 263 | |
| 264 | echo -e "\033[1mForcing garbage cleanup on remote...\033[0m" |
| 265 | if ! ssh -o StrictHostKeyChecking=no \ |
| 266 | -i "${REMOTE_SSH_KEY}" \ |
| 267 | ${REMOTE_SSH_HOST} \ |
| 268 | -p ${REMOTE_SSH_PORT} \ |
| 269 | ${SCRIPT_DIR}/../update.sh -f --gc ; then |
| 270 | >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote" |
| 271 | fi |
| 272 | |
| 273 | echo -e "\e[32mDone\e[0m" |