-
Notifications
You must be signed in to change notification settings - Fork 1
/
backup.sh
executable file
·349 lines (309 loc) · 12.1 KB
/
backup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/bin/bash
# Set defaults
default_args() {
declare -g -A ARGS
ARGS[SHARED_DIR]=/mnt/backups
ARGS[ALPHA_DIR]=/mnt/alpha-browse
ARGS[SOLR]=0
ARGS[ALPHA]=0
ARGS[DB]=0
ARGS[ROTATIONS]=3
ARGS[VERBOSE]=0
}
default_args
# Script help text
runhelp() {
echo ""
echo "Usage: Runs a backup of the Solr and/or the database and"
echo " saves it to the shared storage path."
echo ""
echo "Examples:"
echo " ./backup.sh --solr --db"
echo " Back up both the Solr index and database"
echo " ./backup.sh --solr"
echo " Back up the Solr index"
echo " ./backup.sh --alpha"
echo " Back up the Solr alphabrowse database files"
echo " ./backup.sh --db"
echo " Back up the database"
echo " ./backup.sh --db --rotations 5"
echo " Back up the database saving the last 5 backups"
echo ""
echo "Flags:"
echo " -s|--solr"
echo " Back up the Solr index"
echo " -a|--alpha"
echo " Back up the Solr alphabrowse database files"
echo " -d|--db"
echo " Back up the MariaDB database"
echo " -b|--shared-dir SHARED_DIR"
echo " Full path to the shared storage location for backups to be stored."
echo " Default: ${ARGS[SHARED_DIR]}"
echo " -p|--alpha-dir ALPHA_DIR"
echo " Full path to the alphabrowse database storage location."
echo " Default: ${ARGS[ALPHA_DIR]}"
echo " -r|--rotations ROTATIONS"
echo " Number of most recent backups to save"
echo " Default: ${ARGS[ROTATIONS]}"
echo " -v|--verbose"
echo " Show verbose output"
}
if [[ -z "$1" || $1 == "-h" || $1 == "--help" || $1 == "help" ]]; then
runhelp
exit 0
fi
# Parse command arguments
parse_args() {
# Parse flag arguments
while [[ $# -gt 0 ]]; do
case $1 in
-a|--alpha)
ARGS[ALPHA]=1
shift;;
-s|--solr)
ARGS[SOLR]=1
shift;;
-d|--db)
ARGS[DB]=1
shift;;
-b|--shared-dir)
ARGS[SHARED_DIR]=$( readlink -f "$2" )
RC=$?
if [[ "$RC" -ne 0 || ! -d "${ARGS[SHARED_DIR]}" ]]; then
echo "ERROR: -s|--shared-dir path does not exist: $2"
exit 1
fi
shift; shift ;;
-p|--alpha-dir)
ARGS[ALPHA_DIR]=$( readlink -f "$2" )
RC=$?
if [[ "$RC" -ne 0 || ! -d "${ARGS[ALPHA_DIR]}" ]]; then
echo "ERROR: -p|--alpha-dir path does not exist: $2"
exit 1
fi
shift; shift ;;
-r|--rotations)
ARGS[ROTATIONS]="$2"
if [[ ! "${ARGS[ROTATIONS]}" -gt 0 ]]; then
echo "ERROR: -r|--rotations only accept positive integers"
exit 1
fi
shift; shift ;;
-v|--verbose)
ARGS[VERBOSE]=1
shift;;
*)
echo "ERROR: Unknown flag: $1"
exit 1
esac
done
}
catch_invalid_args() {
if [[ "${ARGS[SOLR]}" -eq 0 && "${ARGS[DB]}" -eq 0 && "${ARGS[ALPHA]}" -eq 0 ]]; then
echo "ERROR: Neither --solr, --alpha, or --db flag is set. Please selet at least one to use this tool."
exit 1
fi
}
# Print message if verbose is enabled
verbose() {
FORCE=$2
LOG_TS=$(date +%Y-%m-%d\ %H:%M:%S)
MSG="[${LOG_TS}] $1"
if [[ "${ARGS[VERBOSE]}" -eq 1 ]] || [[ "$FORCE" -eq 1 ]]; then
echo "${MSG}"
fi
echo "${MSG}" >> "$LOG_FILE"
}
backup_alpha() {
mkdir -p ${ARGS[SHARED_DIR]}/alpha
remove_old_backups ${ARGS[SHARED_DIR]}/alpha
verbose "Taking a backup of the alphabrowse databases"
TIMESTAMP=$( date +%Y%m%d%H%M%S )
mkdir -p ${ARGS[SHARED_DIR]}/alpha/"${TIMESTAMP}"
if ! OUTPUT=$(cp ${ARGS[ALPHA_DIR]}/* ${ARGS[SHARED_DIR]}/alpha/"${TIMESTAMP}"/); then
verbose "ERROR: failed to make a backup of the alphabrowse databases. $OUTPUT" 1
exit 1
fi
verbose "Compressing the backup"
if ! tar -cf ${ARGS[SHARED_DIR]}/alpha/"${TIMESTAMP}".tar -C ${ARGS[SHARED_DIR]}/alpha/"${TIMESTAMP}" --remove-files ./; then
verbose "ERROR: Failed to compress the alphabrowse databases." 1
exit 1
fi
verbose "Completed backup of alphabrowse database"
}
backup_solr() {
backup_collection "biblio"
backup_collection "authority"
backup_collection "reserves"
}
backup_collection() {
# Select one Solr node to perform backups on
SOLR_NODES=(solr1 solr2 solr3)
SOLR_IDX="$(( RANDOM % ${#SOLR_NODES[@]} ))"
SOLR_NODE="${SOLR_NODES[$SOLR_IDX]}"
COLL="$1"
mkdir -p ${ARGS[SHARED_DIR]}/solr/"${COLL}"
mkdir -p ${ARGS[SHARED_DIR]}/solr_dropbox/"${COLL}"
chmod -R 777 ${ARGS[SHARED_DIR]}/solr_dropbox/
# Trigger the backup in Solr
verbose "Starting backup of '${COLL}' index (using node $SOLR_NODE)"
SNAPSHOT="$(date +%Y%m%d%H%M%S)"
if ! OUTPUT=$(curl -sS "http://$SOLR_NODE:8983/solr/${COLL}/replication?command=backup&location=/mnt/solr_backups/${COLL}&name=${SNAPSHOT}"); then
verbose "ERROR: Failed to trigger a backup of the '${COLL}' collection in Solr. Exit code: $?. ${OUTPUT}" 1
exit 1
fi
# Verify that the backup started
sleep 10
SNAPSHOT="snapshot.${SNAPSHOT}"
if [ ! -d "${ARGS[SHARED_DIR]}/solr_dropbox/${COLL}/${SNAPSHOT}" ]; then
verbose "ERROR: Failed to start backup for the '${COLL}' collection in Solr!" 1
exit 1
fi
# Verify that the backup successfully completed
MAX_WAITS=900
CUR_WAIT=1
EXPECTED=""
ACTUAL="0"
while [[ "${EXPECTED}" != "${ACTUAL}" ]]; do
if [ "$CUR_WAIT" -gt "$MAX_WAITS" ]; then
verbose "ERROR: Backup never completed for '${COLL}' index! (${ACTUAL}/${EXPECTED} files copied)" 1
exit 1
fi
if [[ "${EXPECTED}" != "" ]]; then
verbose "Backup still in progress (${ACTUAL}/${EXPECTED} files copied)"
fi
sleep 3
EXPECTED="$(curl -sS "http://$SOLR_NODE:8983/solr/${COLL}/replication?command=details&wt=json" | jq '.details.commits[0][5]|length')"
ACTUAL="$(find ${ARGS[SHARED_DIR]}/solr_dropbox/"${COLL}"/"${SNAPSHOT}" -type f 2>/dev/null | wc -l)"
CUR_WAIT=$((CUR_WAIT+1))
done
# Move the backups from the dropbox, remove Solr's access, and compress
mv ${ARGS[SHARED_DIR]}/solr_dropbox/"${COLL}"/"${SNAPSHOT}" ${ARGS[SHARED_DIR]}/solr/"${COLL}"/
chown -R root:root ${ARGS[SHARED_DIR]}/solr/"${COLL}"
chmod -R 660 ${ARGS[SHARED_DIR]}/solr/"${COLL}"
verbose "Compressing the backup"
PREV_CWD="$(pwd)"
if ! cd ${ARGS[SHARED_DIR]}/solr/"${COLL}"; then
verbose "ERROR: Could not navigate into index backup directory (${ARGS[SHARED_DIR]}/solr/${COLL})" 1
exit 1
fi
if ! tar -c --use-compress-program="pigz -k -p3 " -f "${SNAPSHOT}".tar.gz "${SNAPSHOT}"; then
verbose "ERROR: Failed to compress the backup for the '${COLL}' index." 1
exit 1
else
if ! rm -rf "${SNAPSHOT}"; then
verbose "ERROR: Failed to remove uncompressed backup for the '${COLL}' index." 1
exit 1
fi
fi
if ! cd "${PREV_CWD}"; then
verbose "ERROR: Could not navigate back out of backup directory to previous working dir (${PREV_CWD})" 1
exit 1
fi
verbose "Backup completed for '${COLL}' index."
remove_old_backups ${ARGS[SHARED_DIR]}/solr/"${COLL}"
}
# Return database back to normal
reset_db() {
# DB_NODE is declared in backup_db() function
verbose "Re-enabling Galera node to sychronized state"
if ! OUTPUT=$(mysql -h "$DB_NODE" -u root -p"$MARIADB_ROOT_PASSWORD" -e "SET GLOBAL wsrep_desync = OFF" 2>&1); then
# Check if it was a false negative and the state was actually set
if ! mysql -h "$DB_NODE" -u root -p"$MARIADB_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'wsrep_desync_count'" 2>/dev/null \
| grep 0 > /dev/null 2>&1; then
verbose "ERROR: Failed to re-set node to synchronized state after dump was complete. ${OUTPUT}" 1
exit 1
fi
fi
}
backup_db() {
DBS=( galera1 galera2 galera3 )
DB_IDX="$(( RANDOM % ${#DBS[@]} ))"
declare -g DB_NODE="${DBS[$DB_IDX]}"
mkdir -p ${ARGS[SHARED_DIR]}/db
verbose "Removing leftover uncompressed sql files"
rm ${ARGS[SHARED_DIR]}/db/*.sql 2>/dev/null
# If interrupted, we'll try to reset the database before exiting
trap reset_db SIGTERM SIGINT EXIT
verbose "Temporarily setting Galera node to desychronized state (using node $DB_NODE)"
if ! OUTPUT=$(mysql -h "$DB_NODE" -u root -p"$MARIADB_ROOT_PASSWORD" -e "SET GLOBAL wsrep_desync = ON" 2>&1); then
# Check if it was a false negative and the state was actually set
if ! mysql -h "$DB_NODE" -u root -p"$MARIADB_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'wsrep_desync_count'" 2>/dev/null \
| grep 1 > /dev/null 2>&1; then
verbose "ERROR: Failed to set node to desychronized state. Unsafe to continue backup. ${OUTPUT}" 1
exit 1
fi
fi
remove_old_backups ${ARGS[SHARED_DIR]}/db
verbose "Starting backup of database"
TIMESTAMP=$( date +%Y%m%d%H%M%S )
for DB in "${DBS[@]}"; do
if ! OUTPUT=$(mysqldump -h "${DB}" -u root -p"$MARIADB_ROOT_PASSWORD" --triggers --routines --single-transaction --skip-lock-tables --column-statistics=0 --no-data vufind 2>&1 > >(gzip > ${ARGS[SHARED_DIR]}/db/"${DB}"-"${TIMESTAMP}".sql.gz )); then
verbose "ERROR: Failed to successfully backup the database structure from ${DB}. ${OUTPUT}" 1
exit 1
fi
if ! OUTPUT=$(mysqldump -h "${DB}" -u root -p"$MARIADB_ROOT_PASSWORD" --quick --single-transaction --skip-lock-tables --column-statistics=0 --no-create-info --ignore-table=vufind.session --ignore-table=vufind.SimpleSAMLphp_kvstore --ignore-table=vufind.SimpleSAMLphp_saml_LogoutStore vufind 2>&1 > >(gzip >> ${ARGS[SHARED_DIR]}/db/"${DB}"-"${TIMESTAMP}".sql.gz )); then
verbose "ERROR: Failed to successfully backup the database from ${DB}. ${OUTPUT}" 1
exit 1
fi
done
# Reset the database and unset our trap
reset_db
trap - SIGTERM SIGINT EXIT
verbose "Compressing the backup"
PREV_CWD="$(pwd)"
if ! cd ${ARGS[SHARED_DIR]}/db; then
verbose "ERROR: Could not navigate into database backup directory (${ARGS[SHARED_DIR]}/db)" 1
exit 1
fi
if ! tar -cf "${TIMESTAMP}".tar ./*"${TIMESTAMP}".sql.gz; then
verbose "ERROR: Failed to compress the database dumps." 1
exit 1
else
if ! rm ./*"${TIMESTAMP}.sql.gz"; then
verbose "ERROR: Failed to remove the uncompressed database dumps" 1 # Not exiting
fi
fi
if ! cd "${PREV_CWD}"; then
verbose "ERROR: Could not navigate back out of backup directory to previous working dir (${PREV_CWD})" 1
exit 1
fi
verbose "Completed backup of database"
}
remove_old_backups() {
BACKUP_DIR="$1"
if [ "$(find "${BACKUP_DIR}" -type f \( -name "*.gz" -o -name "*.tar" \) | wc -l)" -ge "${ARGS[ROTATIONS]}" ]; then
verbose "Removing old backups"
CUR_BACKUP=1
find "${BACKUP_DIR}" -type f \( -name "*.gz" -o -name "*.tar" \) -print0 | xargs -0 ls -t | while read -r BACKUP; do
# Remove backups when we have more than the max number of rotations that should be saved
# starting with the oldest backup
if [ "${CUR_BACKUP}" -ge "${ARGS[ROTATIONS]}" ]; then
if [ -f "${BACKUP}" ] && ! rm "${BACKUP}"; then
verbose "ERROR: Could not remove old backup file: ${BACKUP}" 1
fi
fi
CUR_BACKUP=$((CUR_BACKUP+1))
done
fi
}
main() {
declare -g LOG_FILE
LOG_FILE=$(mktemp)
verbose "Logging to ${LOG_FILE}"
verbose "Starting backup of ${STACK_NAME}"
if [[ "${ARGS[SOLR]}" -eq 1 ]]; then
backup_solr
fi
if [[ "${ARGS[DB]}" -eq 1 ]]; then
backup_db
fi
if [[ "${ARGS[ALPHA]}" -eq 1 ]]; then
backup_alpha
fi
verbose "All processing complete"
}
# Parse and start running
parse_args "$@"
catch_invalid_args
main