| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 | #!/usr/bin/env bashPATH=$PATH:/opt/binDATE=$(date +%Y-%m-%d_%H_%M_%S)export LC_ALL=Cechoecho "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."echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."echofunction docker_garbage() {  IMGS_TO_DELETE=()  for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do    REPOSITORY=${container/:*}    TAG=${container/*:}    V_MAIN=${container/*.}    V_SUB=${container/*.}    EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')    for existing_tag in ${EXISTING_TAGS[@]}; do      V_MAIN_EXISTING=${existing_tag/*.}      V_SUB_EXISTING=${existing_tag/*.}      # Not an integer      [[ ! $V_MAIN_EXISTING =~ ^[0-9]+$ ]] && continue      [[ ! $V_SUB_EXISTING =~ ^[0-9]+$ ]] && continue      if [[ $V_MAIN_EXISTING == "latest" ]]; then        echo "Found deprecated label \"latest\" for repository $REPOSITORY, it should be deleted."        IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)      elif [[ $V_MAIN_EXISTING -lt $V_MAIN ]]; then        echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."        IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)      elif [[ $V_SUB_EXISTING -lt $V_SUB ]]; then        echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."        IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)      fi    done  done  if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then    docker rmi ${IMGS_TO_DELETE[*]}  fi}function preflight_local_checks() {  if [[ -z "${REMOTE_SSH_KEY}" ]]; then    >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"    exit 1  fi  if [[ ! -s "${REMOTE_SSH_KEY}" ]]; then    >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} is empty\e[0m"    exit 1  fi  if [[ $(stat -c "%a" "${REMOTE_SSH_KEY}") -ne 600 ]]; then    >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} has insecure permissions\e[0m"    exit 1  fi  if [[ ! -z "${REMOTE_SSH_PORT}" ]]; then    if [[ ${REMOTE_SSH_PORT} != ?(-)+([0-9]) ]] || [[ ${REMOTE_SSH_PORT} -gt 65535 ]]; then      >&2 echo -e "\e[31mREMOTE_SSH_PORT is set but not an integer < 65535\e[0m"      exit 1    fi  fi  if [[ -z "${REMOTE_SSH_HOST}" ]]; then    >&2 echo -e "\e[31mREMOTE_SSH_HOST cannot be empty\e[0m"    exit 1  fi  for bin in rsync docker-compose docker grep cut; do    if [[ -z $(which ${bin}) ]]; then      >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"      exit 1    fi  done  if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then    >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"    exit 1  fi}function preflight_remote_checks() {  ssh -o StrictHostKeyChecking=no \    -i "${REMOTE_SSH_KEY}" \    ${REMOTE_SSH_HOST} \    -p ${REMOTE_SSH_PORT} \    rsync --version > /dev/null  if [ $? -ne 0 ]; then    >&2 echo -e "\e[31mCould not verify connection to ${REMOTE_SSH_HOST}\e[0m"    >&2 echo -e "\e[31mPlease check the output above (is rsync >= 3.1.0 installed on the remote system?)\e[0m"    exit 1  fi  if ssh -o StrictHostKeyChecking=no \    -i "${REMOTE_SSH_KEY}" \    ${REMOTE_SSH_HOST} \    -p ${REMOTE_SSH_PORT} \    grep --help 2>&1 | head -n 1 | grep -q -i "busybox" ; then      >&2 echo -e "\e[31mBusyBox grep detected on remote system ${REMOTE_SSH_HOST}, please install GNU grep\e[0m"      exit 1  fi  for bin in rsync docker-compose docker; do    if ! ssh -o StrictHostKeyChecking=no \      -i "${REMOTE_SSH_KEY}" \      ${REMOTE_SSH_HOST} \      -p ${REMOTE_SSH_PORT} \      which ${bin} > /dev/null ; then        >&2 echo -e "\e[31mCannot find ${bin} in remote PATH, exiting...\e[0m"        exit 1    fi  done}preflight_local_checkspreflight_remote_checksSCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"source "${SCRIPT_DIR}/../mailcow.conf"CMPS_PRJ=$(echo $COMPOSE_PROJECT_NAME | tr -cd "[A-Za-z-_]")SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")echoecho -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"echo# Make sure destination exists, rsync can fail under some circumstancesecho -e "\033[1mPreparing remote...\033[0m"if ! ssh -o StrictHostKeyChecking=no \  -i "${REMOTE_SSH_KEY}" \  ${REMOTE_SSH_HOST} \  -p ${REMOTE_SSH_PORT} \  mkdir -p "${SCRIPT_DIR}/../" ; then    >&2 echo -e "\e[31m[ERR]\e[0m - Could not prepare remote for mailcow base directory transfer"    exit 1fi# Syncing the mailcow base directoryecho -e "\033[1mSynchronizing mailcow base directory...\033[0m"rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \  -i \"${REMOTE_SSH_KEY}\" \  -p ${REMOTE_SSH_PORT}" \  "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../"ec=$?if [ $ec -ne 0 ] && [ $ec -ne 24 ]; then  >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer mailcow base directory to remote"  exit 1fi# Trigger a Redis save for a consistent Redis copyecho -ne "\033[1mRunning redis-cli save... \033[0m"docker exec $(docker ps -qf name=redis-mailcow) redis-cli save# Syncing volumes related to compose project# Same here: make sure destination existsfor vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do  mountpoint="$(docker inspect $vol | grep Mountpoint | cut -d '"' -f4)"  echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m"  ssh -o StrictHostKeyChecking=no \    -i "${REMOTE_SSH_KEY}" \    ${REMOTE_SSH_HOST} \    -p ${REMOTE_SSH_PORT} \    mkdir -p "${mountpoint}"  if [[ "${vol}" =~ "mysql-vol-1" ]]; then    # Make sure a previous backup does not exist    rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"    echo -e "\033[1mCreating consistent backup of MariaDB volume...\033[0m"    if ! docker run --rm \      --network $(docker network ls -qf name=${CMPS_PRJ}_) \      -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \      --entrypoint= \      -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \      ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then        >&2 echo -e "\e[31m[ERR]\e[0m - Could not create MariaDB backup on source"        rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"        exit 1    fi    if ! docker run --rm \      --network $(docker network ls -qf name=${CMPS_PRJ}_) \      --entrypoint= \      -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \      ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then        >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"        rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"        exit 1    fi    chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup"    echo -e "\033[1mSynchronizing MariaDB backup...\033[0m"    rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \      -i \"${REMOTE_SSH_KEY}\" \      -p ${REMOTE_SSH_PORT}" \      "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}"    ec=$?    if [ $ec -ne 0 ] && [ $ec -ne 24 ]; then      >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote"      exit 1    fi    # Cleanup    rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"  else    echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"    rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \      -i \"${REMOTE_SSH_KEY}\" \      -p ${REMOTE_SSH_PORT}" \      "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"    ec=$?    if [ $ec -ne 0 ] && [ $ec -ne 24 ]; then      >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer ${vol} from local ${mountpoint} to remote"      exit 1    fi  fi  echo -e "\e[32mCompleted\e[0m"done# Restart Dockerd on destinationecho -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"if ! ssh -o StrictHostKeyChecking=no \  -i "${REMOTE_SSH_KEY}" \  ${REMOTE_SSH_HOST} \  -p ${REMOTE_SSH_PORT} \  systemctl restart docker ; then    >&2 echo -e "\e[31m[ERR]\e[0m - Could not restart Docker daemon on remote"    exit 1fiecho "OK"echo -e "\033[1mPulling images on remote...\033[0m"if ! ssh -o StrictHostKeyChecking=no \  -i "${REMOTE_SSH_KEY}" \  ${REMOTE_SSH_HOST} \  -p ${REMOTE_SSH_PORT} \  docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel 2>&1 ; then    >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"fiecho -e "\033[1mForcing garbage cleanup on remote...\033[0m"if ! ssh -o StrictHostKeyChecking=no \  -i "${REMOTE_SSH_KEY}" \  ${REMOTE_SSH_HOST} \  -p ${REMOTE_SSH_PORT} \  ${SCRIPT_DIR}/../update.sh -f --gc ; then    >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"fiecho -e "\e[32mDone\e[0m"
 |