blob: 0e8885a3ec43f96ecddc1414dd05ec246e9c8c01 [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001#!/usr/bin/env bash
2
3PATH=${PATH}:/opt/bin
4DATE=$(date +%Y-%m-%d_%H_%M_%S)
5export LC_ALL=C
6
7echo
8echo "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."
9echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."
10echo
11
12function 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
52function 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 Benkard1ba53812022-12-27 17:32:58 +010080 for bin in rsync docker grep cut; do
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010081 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 Benkard1ba53812022-12-27 17:32:58 +010088 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010089 exit 1
90 fi
91}
92
93function 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 Benkard1ba53812022-12-27 17:32:58 +0100114 for bin in rsync docker; do
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100115 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 Benkard1ba53812022-12-27 17:32:58 +0100125 ssh -o StrictHostKeyChecking=no \
126 -i "${REMOTE_SSH_KEY}" \
127 ${REMOTE_SSH_HOST} \
128 -p ${REMOTE_SSH_PORT} \
129 "bash -s" << "EOF"
130if docker compose > /dev/null 2>&1; then
131 exit 0
132elif docker-compose version --short | grep "^2." > /dev/null 2>&1; then
133 exit 1
134else
135exit 2
136fi
137EOF
138
139if [ $? = 0 ]; then
140 COMPOSE_COMMAND="docker compose"
141 echo "DEBUG: Using native docker compose on remote"
142
143elif [ $? = 1 ]; then
144 COMPOSE_COMMAND="docker-compose"
145 echo "DEBUG: Using standalone docker compose on remote"
146
147else
148 echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
149 exit 1
150fi
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100151}
152
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100153SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
154source "${SCRIPT_DIR}/../mailcow.conf"
155COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
156CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
157SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
158
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100159preflight_local_checks
160preflight_remote_checks
161
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100162echo
163echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
164echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
165echo
166
167# Make sure destination exists, rsync can fail under some circumstances
168echo -e "\033[1mPreparing remote...\033[0m"
169if ! 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
176fi
177
178# Syncing the mailcow base directory
179echo -e "\033[1mSynchronizing mailcow base directory...\033[0m"
180rsync --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}/../"
184ec=$?
185if [ ${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
188fi
189
190# Trigger a Redis save for a consistent Redis copy
191echo -ne "\033[1mRunning redis-cli save... \033[0m"
192docker 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
196for 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
267done
268
269# Restart Dockerd on destination
270echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"
271if ! 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
278fi
279echo "OK"
280
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100281 echo -e "\e[33mPulling images on remote...\e[0m"
282 echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m"
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100283
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100284 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
292echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m"
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100293if ! 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"
299fi
300
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100301echo -e "\e[32mDone\e[0m"