ansible-roles/mysql/files/evomariabackup.sh

715 lines
20 KiB
Bash
Raw Normal View History

2021-11-01 10:16:52 +01:00
#!/bin/sh
2022-06-05 21:49:23 +02:00
VERSION="22.06.1"
2021-11-01 10:16:52 +01:00
show_version() {
cat <<END
evomariabackup version ${VERSION}
2022-06-02 18:27:59 +02:00
Copyright 2004-2022 Evolix <info@evolix.fr>,
2021-11-01 10:16:52 +01:00
Éric Morino <emorino@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>
and others.
evomariabackup comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the GNU Affero General Public License v3.0 for details.
END
}
show_help() {
cat <<EOF
Usage: evomariabackup --backup-dir /path/to/mariabackup-target --compress-file /path/to/compressed.tgz
Options
2022-06-02 18:27:59 +02:00
--backup Force backup phase
--no-backup Skip backup phase
--backup-dir mariabackup target directory
it enables backup phase if not explicitly disabled
2022-06-02 18:27:59 +02:00
--compress Force compress phase
--no-compress Skip compress phase
--compress-file File name for the compressed version
it enables backup phase if not explicitly disabled
--mtree Force mtree phase
--no-mtree Skip mtree phase
--mtree-file File name for the mtree specification
it enables mtree phase if not explicitly disabled
2022-06-02 18:27:59 +02:00
--log-file Log file to send messages
--post-backup-hook Script to execute after other tasks
--verbose Output much more information (to stdout/stderr or the log file)
--quiet Ouput only the most critical information
--lock-file Specify which lock file to use (default: /run/lock/mariabackup.lock)
--max-age Lock file is ignored if older than this (default: 1d)
--force-unlock If a lock is present, do as if it has expired
2022-06-02 18:27:59 +02:00
-h|--help|-? Display help
-V|--version Display version, authors and license
2021-11-01 10:16:52 +01:00
Example usage for a backup, mtree then compress :
2021-11-01 10:16:52 +01:00
# /usr/local/bin/evomariabackup --verbose \
--backup --backup-dir /backup/mariabackup/current \
--mtree --mtree-file /backup/mariabackup/current.mtree \
--compress --compress-file /backup/mariabackup/compressed/$(date +\%H).tgz \
2021-11-01 10:16:52 +01:00
--log-file /var/log/evomariabackup.log
max-age possible values:
Xd = X days
X or Xh = X hours
Xm = X minutes
Xs = X seconds
EOF
}
log_date() {
date +"%Y-%m-%d %H:%M:%S"
}
is_log_file() {
test -n "${log_file}"
}
is_verbose() {
test "${verbose}" = "1"
}
is_quiet() {
test "${quiet}" = "1"
}
log_line() {
level=$1
msg=$2
printf "[%s] %s: %s\n" "$(log_date)" "${level}" "${msg}"
}
log_debug() {
level="DEBUG"
msg=$1
if ! is_quiet && is_verbose; then
if is_log_file; then
log_line "${level}" "${msg}" >> "${log_file}"
else
log_line "${level}" "${msg}" >&2
fi
fi
}
log_info() {
level="INFO"
msg=$1
if ! is_quiet; then
if is_log_file; then
log_line "${level}" "${msg}" >> "${log_file}"
else
log_line "${level}" "${msg}" >&2
fi
fi
}
log_warning() {
level="WARNING"
msg=$1
if ! is_quiet; then
if is_log_file; then
log_line "${level}" "${msg}" >> "${log_file}"
else
log_line "${level}" "${msg}" >&2
fi
fi
}
log_error() {
level="ERROR"
msg=$1
if ! is_quiet; then
if is_log_file; then
log_line "${level}" "${msg}" >> "${log_file}"
if tty -s; then
printf "%s\n" "${msg}" >&2
fi
else
log_line "${level}" "${msg}" >&2
fi
fi
}
log_fatal() {
level="FATAL"
msg=$1
if is_log_file; then
log_line "${level}" "${msg}" >> "${log_file}"
if tty -s; then
printf "%s\n" "${msg}" >&2
fi
else
log_line "${level}" "${msg}" >&2
fi
}
duration_in_seconds() {
if echo "${1}" | grep -E -q '^([0-9]+[wdhms])+$'; then
duration=$(echo "${1}" | sed 's/w/ * 604800 + /g; s/d/ * 86400 + /g; s/h/ * 3600 + /g; s/m/ * 60 + /g; s/s/ + /g; s/+ $//' | xargs expr)
elif echo "${1}" | grep -E -q '^[0-9]+$'; then
duration=$(echo "${1} * 3600" | xargs expr)
else
return 1
fi
log_debug "Duration \`${1}' translated to ${duration} seconds"
echo "${duration}"
}
lock_file_created_at() {
created_at=$(stat -c %Z "${lock_file}")
log_debug "Lock file \`${lock_file}' created at ${created_at}"
2021-11-01 10:16:52 +01:00
echo "${created_at}"
}
lock_file_age() {
last_change=$(lock_file_created_at)
now=$(date +"%s")
age=$(( now - last_change ))
log_debug "Lock file \`${lock_file}' is ${age} seconds old"
2021-11-01 10:16:52 +01:00
echo "${created_at}"
}
is_force_unlock() {
test "${force_unlock}" = "1"
}
2021-11-01 10:16:52 +01:00
is_lock_file_too_old() {
test "$(lock_file_age)" -ge "${max_age}"
}
kill_or_clean_lockfile() {
lock_file=${1:-}
if [ -f "${lock_file}" ]; then
# Get Process ID from the lock file
pid=$(cat "${lock_file}")
if [ -n "${pid}" ]; then
log_debug "Found pid ${pid} in ${lock_file}"
if kill -0 "${pid}" 2> /dev/null; then
log_debug "Found process with pid ${pid}"
lock_file_created_at_human=$(date --date "@$(lock_file_created_at)" +"%Y-%m-%d %H:%M:%S")
if is_lock_file_too_old || is_force_unlock ; then
2021-11-01 10:16:52 +01:00
# Kill the children
pkill -9 --parent "${pid}"
# Kill the parent
kill -9 "${pid}"
# Only one process can run in parallel
if is_lock_file_too_old; then
unlock_reason="lock is older than ${max_age}"
elif is_force_unlock; then
unlock_reason="--force-unlock was used"
else
unlock_reason="unknown reason"
fi
log_warning "Process \`${pid}' (started at ${lock_file_created_at_human}) has been killed by \`$$' (${unlock_reason})."
2021-11-01 10:16:52 +01:00
else
log_info "Process \`${pid}' (started at ${lock_file_created_at_human}) has precedence. Let's leave it work."
# make sure that this exit doesn't remove the existing lockfile !!
exit 0
fi
else
log_warning "Process not found at PID \`${pid}'. Ignoring lock file \`${lock_file}'."
fi
else
log_warning "Empty lockfile \`${lock_file}'. It should contain a PID."
fi
# Remove the lock file
rm -f "${lock_file}"
log_debug "Lock file \`${lock_file}' has been removed"
2021-11-01 10:16:52 +01:00
fi
}
new_lock_file() {
lock_file=${1:-}
lock_dir=$(dirname "${lock_file}")
if mkdir --parents "${lock_dir}"; then
echo $$ > "${lock_file}"
log_debug "Lock file \`${lock_file}' has been created"
2021-11-01 10:16:52 +01:00
else
log_fatal "Failed to acquire lock file \`${lock_file}'. Abort."
2021-11-01 10:16:52 +01:00
exit 1
fi
}
is_mariabackup_directory() {
directory=${1:-}
find "${directory}" -name 'ibdata*' -o -name 'ib_logfile*' -o -name 'xtrabackup_*' > /dev/null
}
check_backup_dir() {
if [ -d "${backup_dir:?}" ]; then
if [ "$(ls -A "${backup_dir:?}")" ]; then
if is_mariabackup_directory "${backup_dir:?}"; then
log_debug "The backup directory \`${backup_dir:?}' is not empty but looks like a mariabackup target. Let's clear it."
2021-11-01 10:16:52 +01:00
rm -rf "${backup_dir:?}"
else
log_fatal "The backup directory \`${backup_dir:?}' is not empty and doesn't look like a mariabackup target. Please verify and clear the directory if you are sure."
2021-11-01 10:16:52 +01:00
exit 1
fi
else
log_debug "The backup directory \`${backup_dir:?}' exists but is empty. Let's proceed."
2021-11-01 10:16:52 +01:00
fi
else
log_debug "The backup directory \`${backup_dir:?}' doesn't exist. Let's proceed."
2021-11-01 10:16:52 +01:00
fi
mkdir -p "${backup_dir:?}"
}
check_compress_dir() {
if [ -d "${compress_dir:?}" ]; then
log_debug "The compress_dir directory \`${compress_dir:?}' exists. Let's proceed."
2021-11-01 10:16:52 +01:00
else
log_debug "The compress_dir directory \`${compress_dir:?}' doesn't exist. Let's proceed."
2021-11-01 10:16:52 +01:00
fi
mkdir -p "${compress_dir:?}"
}
backup_phase() {
2021-11-01 10:16:52 +01:00
if [ -z "${backup_dir}" ]; then
log_fatal "backup-dir option is empty"
else
check_backup_dir
fi
mariabackup_bin=$(command -v mariabackup)
if [ -z "${mariabackup_bin}" ]; then
log_fatal "Couldn't find mariabackup.\nYou can install it with 'apt install mariadb-backup'."
2021-11-01 10:16:52 +01:00
exit 1
fi
backup_command="${mariabackup_bin} --backup --slave-info --target-dir=${backup_dir:?}"
if ! is_quiet; then
log_info "BEGIN mariabackup backup phase"
2022-06-02 18:25:12 +02:00
log_debug "${backup_command}"
2021-11-01 10:16:52 +01:00
fi
if is_quiet || ! is_verbose ; then
${backup_command} >/dev/null 2>&1
backup_rc=$?
elif ! is_quiet; then
if is_log_file; then
${backup_command} >>"${log_file}" 2>&1
backup_rc=$?
else
${backup_command}
backup_rc=$?
fi
fi
if [ ${backup_rc} -ne 0 ]; then
log_fatal "Error executing mariabackup --backup"
exit 1
elif ! is_quiet; then
log_info "END mariabackup backup phase"
fi
prepare_command="${mariabackup_bin} --prepare --target-dir=${backup_dir:?}"
if ! is_quiet; then
log_info "BEGIN mariabackup prepare phase"
2022-06-02 18:25:12 +02:00
log_debug "${prepare_command}"
2021-11-01 10:16:52 +01:00
fi
if is_quiet || ! is_verbose ; then
${prepare_command} >/dev/null 2>&1
prepare_rc=$?
elif ! is_quiet; then
if is_log_file; then
${prepare_command} >>"${log_file}" 2>&1
prepare_rc=$?
else
${prepare_command}
prepare_rc=$?
fi
fi
if [ ${prepare_rc} -ne 0 ]; then
log_fatal "Error executing mariabackup --prepare"
exit 1
elif ! is_quiet; then
log_info "END mariabackup prepare phase"
fi
}
mtree_phase() {
if [ -z "${backup_dir}" ]; then
log_fatal "backup-dir option is empty"
exit 1
elif [ -e "${backup_dir}" ] && [ ! -d "${backup_dir}" ]; then
log_fatal "backup directory \`${backup_dir}' exists but is not a directory"
exit 1
fi
if [ -z "${mtree_file}" ]; then
mtree_file="${backup_dir}.mtree"
fi
mtree_cmd="mtree"
mtree_bin=$(command -v ${mtree_cmd})
if [ -z "${mtree_bin}" ]; then
log_fatal "Couldn't find ${mtree_cmd}.\nYou can install it with 'apt install mtree-netbsd'."
exit 1
fi
backup_parent_dir=$(dirname "${backup_dir}")
backup_final_dir=$(basename "${backup_dir}")
log_info "BEGIN mtree phase"
log_debug "Store mtree specification of \`${backup_dir}' to \`${mtree_file}' using \`${mtree_bin}'"
"${mtree_bin}" -x -c -p "${backup_dir}" > "${mtree_file}"
log_info "END mtree phase"
}
compress_phase() {
2021-11-01 10:16:52 +01:00
compress_dir=$(dirname "${compress_file}")
if [ -z "${backup_dir}" ]; then
log_fatal "backup-dir option is empty"
exit 1
elif [ -e "${backup_dir}" ] && [ ! -d "${backup_dir}" ]; then
log_fatal "backup directory \`${backup_dir}' exists but is not a directory"
2021-11-01 10:16:52 +01:00
exit 1
fi
if [ -z "${compress_file}" ]; then
log_fatal "compress-file option is empty"
exit 1
fi
if [ -n "${compress_dir}" ]; then
check_compress_dir
fi
pigz_bin=$(command -v pigz)
gzip_bin=$(command -v gzip)
if [ -n "${pigz_bin}" ]; then
compress_program="${pigz_bin} --keep -6"
elif [ -n "${gzip_bin}" ]; then
compress_program="${gzip_bin} -6"
else
log_fatal "Couldn't find pigz nor gzip.\nYou can install it with 'apt install pigz' or 'apt install gzip'."
2021-11-01 10:16:52 +01:00
exit 1
fi
if ! is_quiet; then
log_info "BEGIN compression phase"
log_debug "Compression of \`${backup_dir}' to \`${compress_file}' using \`${compress_program}'"
2021-11-01 10:16:52 +01:00
fi
if is_quiet || ! is_verbose ; then
tar --use-compress-program="${compress_program}" -cf "${compress_file}" "${backup_dir}" >/dev/null 2>&1
tar_rc=$?
elif ! is_quiet; then
if is_log_file; then
tar --use-compress-program="${compress_program}" -cf "${compress_file}" "${backup_dir}" >>"${log_file}" 2>&1
tar_rc=$?
else
tar --use-compress-program="${compress_program}" -cf "${compress_file}" "${backup_dir}"
tar_rc=$?
fi
fi
if [ ${tar_rc} -ne 0 ]; then
log_fatal "An error occured while compressing \`${backup_dir}' to \`${compress_file}'"
2021-11-01 10:16:52 +01:00
exit 1
elif ! is_quiet; then
log_info "END compression phase"
fi
}
post_backup_hook_phase() {
if [ -x "${post_backup_hook}" ]; then
if ! is_quiet; then
log_debug "Execution of \`${post_backup_hook}'"
log_info "BEGIN hook phase"
fi
(
export BACKUP_DIR="${backup_dir}"
if is_log_file; then
export LOG_FILE="${log_file}"
fi
"${post_backup_hook}"
)
hook_rc=$?
if [ ${hook_rc} -ne 0 ]; then
log_fatal "An error occured while executing post backup hook \`${post_backup_hook}'"
exit 1
elif ! is_quiet; then
log_info "END hook phase"
fi
else
log_fatal "Post backup hook \`${post_backup_hook}' is missing or not executable"
exit 1
fi
}
2021-11-01 10:16:52 +01:00
main() {
2022-06-03 11:09:38 +02:00
if ! is_quiet; then
log_info "BEGIN evomariabackup"
fi
2021-11-01 10:16:52 +01:00
kill_or_clean_lockfile "${lock_file}"
# shellcheck disable=SC2064
trap "rm -f ${lock_file};" 0
new_lock_file "${lock_file}"
if [ "${do_backup}" = "1" ] && [ -n "${backup_dir}" ]; then
backup_phase
fi
if [ "${do_mtree}" = "1" ] && [ -n "${backup_dir}" ]; then
mtree_phase
2021-11-01 10:16:52 +01:00
fi
if [ "${do_compress}" = "1" ] && [ -n "${compress_file}" ]; then
compress_phase
2021-11-01 10:16:52 +01:00
fi
if [ -n "${post_backup_hook}" ]; then
post_backup_hook_phase
fi
2022-06-03 11:09:38 +02:00
if ! is_quiet; then
log_info "END evomariabackup"
fi
2021-11-01 10:16:52 +01:00
}
# Declare variables
lock_file=""
log_file=""
verbose=""
quiet=""
max_age=""
force_unlock=""
2021-11-01 10:16:52 +01:00
do_backup=""
backup_dir=""
do_mtree=""
mtree_file=""
2021-11-01 10:16:52 +01:00
do_compress=""
compress_file=""
post_backup_hook=""
2021-11-01 10:16:52 +01:00
# Parse options
# based on https://gist.github.com/deshion/10d3cb5f88a21671e17a
while :; do
case $1 in
-h|-\?|--help)
show_help
exit 0
;;
-V|--version)
show_version
exit 0
;;
-m|--max-age)
# with value separated by space
if [ -n "$2" ]; then
max_age=$(duration_in_seconds "$2")
shift
else
log_fatal 'ERROR: "-m|--max-age" requires a non-empty option argument.'
fi
;;
--max-age=?*)
# with value speparated by =
max_age=$(duration_in_seconds "${1#*=}")
;;
--max-age=)
# without value
log_fatal 'ERROR: "--max-age" requires a non-empty option argument.'
;;
--backup)
do_backup=1
;;
--no-backup)
do_backup=0
;;
--backup-dir)
# with value separated by space
if [ -n "$2" ]; then
backup_dir="$2"
shift
else
log_fatal 'ERROR: "--backup-dir" requires a non-empty option argument.'
fi
;;
--backup-dir=?*)
# with value speparated by =
backup_dir=${1#*=}
;;
--backup-dir=)
# without value
log_fatal '"--backup-dir" requires a non-empty option argument.'
;;
--compress)
do_compress=1
;;
--no-compress)
do_compress=0
;;
--compress-file)
# with value separated by space
if [ -n "$2" ]; then
compress_file="$2"
if [ -z "${do_compress}" ]; then
do_compress=1
fi
shift
else
log_fatal '"--compress-file" requires a non-empty option argument.'
fi
;;
--compress-file=?*)
# with value speparated by =
compress_file=${1#*=}
if [ -z "${do_compress}" ]; then
do_compress=1
fi
;;
--compress-file=)
# without value
log_fatal '"--compress-file" requires a non-empty option argument.'
;;
--mtree)
do_mtree=1
;;
--no-mtree)
do_mtree=0
;;
--mtree-file)
# with value separated by space
if [ -n "$2" ]; then
mtree_file="$2"
if [ -z "${do_mtree}" ]; then
do_mtree=1
fi
shift
else
log_fatal '"--mtree-file" requires a non-empty option argument.'
fi
;;
--mtree-file=?*)
# with value speparated by =
mtree_file=${1#*=}
if [ -z "${do_mtree}" ]; then
do_mtree=1
fi
;;
--mtree-file=)
# without value
log_fatal '"--mtree-file" requires a non-empty option argument.'
;;
2021-11-01 10:16:52 +01:00
--lock-file)
# with value separated by space
if [ -n "$2" ]; then
lock_file="$2"
shift
else
log_fatal '"--lock-file" requires a non-empty option argument.'
fi
;;
--lock-file=?*)
# with value speparated by =
lock_file=${1#*=}
;;
--lock-file=)
# without value
log_fatal '"--lock-file" requires a non-empty option argument.'
;;
--force-unlock)
force_unlock=1
;;
2021-11-01 10:16:52 +01:00
--log-file)
# with value separated by space
if [ -n "$2" ]; then
log_file="$2"
shift
else
log_fatal '"--log-file" requires a non-empty option argument.'
fi
;;
--log-file=?*)
# with value speparated by =
log_file=${1#*=}
;;
--log-file=)
# without value
log_fatal '"--log-file" requires a non-empty option argument.'
;;
--post-backup-hook)
# with value separated by space
if [ -n "$2" ]; then
post_backup_hook="$2"
shift
else
log_fatal '"--post-backup-hook" requires a non-empty option argument.'
fi
;;
--post-backup-hook=?*)
# with value speparated by =
post_backup_hook=${1#*=}
;;
--post-backup-hook=)
# without value
log_fatal '"--post-backup-hook" requires a non-empty option argument.'
;;
2021-11-01 10:16:52 +01:00
-v|--verbose)
verbose=1
;;
--quiet)
quiet=1
verbose=0
;;
--)
# End of all options.
shift
break
;;
-?*|[[:alnum:]]*)
# ignore unknown options
if tty -s; then
printf 'Unknown option : %s\n' "$1" >&2
echo "" >&2
show_help >&2
2021-11-01 10:16:52 +01:00
exit 1
else
log_fatal 'Unknown option : %s\n' "$1" >&2
fi
;;
*)
# Default case: If no more options then break out of the loop.
break
;;
esac
shift
done
# Default values
lock_file="${lock_file:-/run/lock/evomariabackup.lock}"
verbose=${verbose:-0}
quiet=${quiet:-0}
max_age="${max_age:-1d}"
force_unlock=${force_unlock:-0}
# Enable backup phase if not disabled and backup_dir is set
if [ -z "${do_backup}" ] && [ -n "${backup_dir}" ]; then
do_backup="1"
fi
# Enable mtree phase if not disabled and mtree_file is set
if [ -z "${do_mtree}" ] && [ -n "${mtree_file}" ]; then
do_mtree="1"
fi
# Enable compress phase if not disabled and compress_file is set
if [ -z "${do_compress}" ] && [ -n "${compress_file}" ]; then
do_compress="1"
fi
2021-11-01 10:16:52 +01:00
main