#!/bin/sh VERSION="21.11" show_version() { cat <, Éric Morino , Jérémy Lecour 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 <> "${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}" 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" echo "${created_at}" } 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 ; then # Kill the children pkill -9 --parent "${pid}" # Kill the parent kill -9 "${pid}" # Only one process can run in parallel log_warning "Process \`${pid}' (started at ${lock_file_created_at_human}) has been killed by \`$$'" 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" 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" else log_fatal "Failed to acquire lock file '${lock_file}'. Abort." 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." 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." exit 1 fi else log_debug "The backup directory ${backup_dir:?} exists but is empty. Let's proceed." fi else log_debug "The backup directory ${backup_dir:?} doesn't exist. Let's proceed." 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." else log_debug "The compress_dir directory ${compress_dir:?} doesn't exist. Let's proceed." fi mkdir -p "${compress_dir:?}" } backup() { 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.\nUse 'apt install mariadb-backup'." exit 1 fi backup_command="${mariabackup_bin} --backup --slave-info --target-dir=${backup_dir:?}" if ! is_quiet; then log_debug "${backup_command}" log_info "BEGIN mariabackup backup phase" 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_debug "${prepare_command}" log_info "BEGIN mariabackup prepare phase" 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 } compress() { 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" 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.\nUse 'apt install pigz' or 'apt install gzip'." exit 1 fi if ! is_quiet; then log_debug "Compression of ${backup_dir} to ${compress_file} using \`${compress_program}'" log_info "BEGIN compression phase" 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}" exit 1 elif ! is_quiet; then log_info "END compression phase" fi } main() { 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 "${backup_dir}" fi if [ "${do_compress}" = "1" ] && [ -n "${compress_file}" ]; then compress "${backup_dir}" "${compress_file}" fi } # Declare variables lock_file="" log_file="" verbose="" quiet="" max_age="" max_age="" do_backup="" backup_dir="" do_compress="" compress_file="" # 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.' ;; --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.' ;; --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.' ;; -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_usage >&2 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:-86400}" do_backup="${do_backup:-1}" do_compress="${do_compress:-0}" main