blob: c4439ea814ab3730027b087df4c43c13c895673b [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
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
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
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
127preflight_local_checks
128preflight_remote_checks
129
130SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
131COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
132source "${SCRIPT_DIR}/../mailcow.conf"
133CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
134SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
135
136echo
137echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
138echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
139echo
140
141# Make sure destination exists, rsync can fail under some circumstances
142echo -e "\033[1mPreparing remote...\033[0m"
143if ! 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
150fi
151
152# Syncing the mailcow base directory
153echo -e "\033[1mSynchronizing mailcow base directory...\033[0m"
154rsync --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}/../"
158ec=$?
159if [ ${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
162fi
163
164# Trigger a Redis save for a consistent Redis copy
165echo -ne "\033[1mRunning redis-cli save... \033[0m"
166docker 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
170for 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
241done
242
243# Restart Dockerd on destination
244echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m"
245if ! 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
252fi
253echo "OK"
254
255echo -e "\033[1mPulling images on remote...\033[0m"
256if ! 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"
262fi
263
264echo -e "\033[1mForcing garbage cleanup on remote...\033[0m"
265if ! 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"
271fi
272
273echo -e "\e[32mDone\e[0m"