#!/usr/bin/env bash

# Copyright (C) 2015-2019 Maciej Delmanowski <drybjed@gmail.com>
# Copyright (C) 2015-2019 DebOps <https://debops.org/>
# SPDX-License-Identifier: GPL-3.0-only

# Manage rsnapshot backups configured in /etc/rsnapshot/hosts/


display_usage () {

    cat <<EOF
Usage: $(basename "${0}") <run|schedule> <level> <all|host>

run           start the backup of a given host on a given level
schedule      create a backup job with given parameters

<level>       name of a rsnapshot backup level to use
              (hourly, daily, weekly, monthly, etc.)

all           schedule backup job for all hosts
<host>        name of the host directory in /etc/rsnapshot
EOF

}

script="${0}"

scheduler_config="/etc/$(basename "${script}").conf"

scheduler_config_dir="/etc/rsnapshot/hosts"
scheduler_snapshot_root="/var/cache/rsnapshot/hosts"
scheduler_lock_dir="/run/rsnapshot-scheduler"
scheduler_type="batch"
scheduler_batch_command="at -q R now"
scheduler_sleep_max_delay="20"
scheduler_run_max_delay="5"

# shellcheck disable=SC1090
[ -r "${scheduler_config}" ] && . "${scheduler_config}"

pid="$$"

arg_mode="${1}"
arg_backup_level="${2}"
arg_host="${3}"

log_message () {
    if tty -s > /dev/null 2>&1 ; then
        echo "$(basename "${script}"): ${*}"
    elif type logger > /dev/null 2>&1 ; then
        logger -t "$(basename "${script}")[${pid}]" "${*}"
    fi
}

if [ -r "${scheduler_config_dir}/stop" ] || [ -r "${scheduler_config_dir}/disable" ] || [ -r "${scheduler_config_dir}/disabled" ] ; then
    log_message "Exit: rsnapshot backups are disabled globally"
    exit 0
fi

if [ -r "${scheduler_snapshot_root}/stop" ] || [ -r "${scheduler_snapshot_root}/disable" ] || [ -r "${scheduler_snapshot_root}/disabled" ] ; then
    log_message "Exit: rsnapshot backups are disabled globally via snapshot root directory"
    exit 0
fi

if [ -z "${arg_mode}" ] ; then
    display_usage
    exit 0
fi

if [ -z "${arg_backup_level}" ] ; then
    log_message "Error: backup level not specified"
    exit 1
fi

if [ -z "${arg_host}" ] ; then
    log_message "Error: host not specified"
    exit 1
fi

clean_up () {

    local backup_level="${1}"
    local host="${2}"

    local scheduler_pidfile
    scheduler_pidfile="${scheduler_lock_dir}/$(basename "${script}")-${backup_level}-${host}.pid"

    test -f "${scheduler_pidfile}" && rm -f "${scheduler_pidfile}"
}

wait_for_pid () {

    local backup_level="${1}"
    local host="${2}"

    local scheduler_pidfile
    scheduler_pidfile="${scheduler_lock_dir}/$(basename "${script}")-${backup_level}-${host}.pid"

    if [ -f "${scheduler_pidfile}" ] ; then
        while kill -0 "$(<"${scheduler_pidfile}")" > /dev/null 2>&1 ; do
            log_message "Scheduled ${backup_level} backup of host ${host} is waiting for PID $(<"${scheduler_pidfile}") to finish"
            sleep $(( ( RANDOM % scheduler_run_max_delay ) + 1))m
        done
        test -f "${scheduler_pidfile}" && rm -f "${scheduler_pidfile}"
    fi

}

# shellcheck disable=SC2120
delay_backup () {

    local delay="${1}"

    if [ -n "${delay}" ] ; then
        sleep "${delay}"
    else
        sleep $(( ( RANDOM % scheduler_sleep_max_delay ) + 1 ))m
    fi
}

run_backup () {

    local backup_level="${1}"
    local host="${2}"

    local scheduler_pidfile
    scheduler_pidfile="${scheduler_lock_dir}/$(basename "${script}")-${backup_level}-${host}.pid"

    if [ "${host}" == "all" ] ; then
        log_message "Error: cannot backup host ${host}"
        exit 1
    fi

    if [ -r "${scheduler_config_dir}/${host}/rsnapshot.conf" ] ; then
        local config="${scheduler_config_dir}/${host}/rsnapshot.conf"
    else
        log_message "Error: configuration for ${host} not found"
        exit 1
    fi

    if [ -r "${scheduler_config_dir}/${host}/stop" ] || [ -r "${scheduler_config_dir}/${host}/disable" ] || [ -r "${scheduler_config_dir}/${host}/disabled" ] ; then
        log_message "Exit: backup of ${host} is disabled"
        exit 0
    fi

    if [ -r "${config}" ] ; then

        read -r -a snapshot_root <<< "$(grep -E "^snapshot_root\\s+" "${config}" | awk '{print $2}' | head -n 1)"
        read -r -a no_create_root <<< "$(grep -E "^no_create_root\\s+1" "${config}")"
        read -r -a sync_first <<< "$(grep -E "^sync_first\\s+1" "${config}")"
        read -r -a retain_first <<< "$(grep -E "^retain\\s+" "${config}" | awk '{print $2}' | head -n 1)"
        read -r -a retain_name <<< "$(grep -E "^retain\\s+${backup_level}\\s+" "${config}" | awk '{print $2}' | head -n 1)"
        read -r -a lockfile <<< "$(grep -E "^lockfile\\s+" "${config}" | awk '{print $2}' | head -n 1)"

        # If no_create_root is enabled, check the existence of the root backup
        # directory early so that we can avoid spamming the system
        # administrator with e-mails about missing root directory.
        if [ -n "${no_create_root[*]}" ] ; then
            if [ -n "${snapshot_root[*]}" ] && [ ! -d "${snapshot_root[*]}" ] ; then
                log_message "Exit: Backup of ${host} at ${backup_level} level stopped, snapshot directory not found and will not be created automatically"
                exit 0
            fi
        fi

        if [ "${retain_name[*]}" == "${backup_level[*]}" ] ; then

            if [ -f "${scheduler_pidfile}" ] ; then
                if kill -0 "$(<"${scheduler_pidfile}")" > /dev/null 2>&1 ; then
                    log_message "Exit: Backup of ${host} at ${backup_level} level already in progress, managed by PID $(cat "${scheduler_pidfile}")"
                    exit 0
                else
                    rm -f "${scheduler_pidfile}"
                fi
            fi

            # shellcheck disable=SC2174
            test -d "$(dirname "${scheduler_pidfile}")" || mkdir -p -m 755 "$(dirname "${scheduler_pidfile}")"
            echo ${pid} > "${scheduler_pidfile}"
            sleep 1

            if [ "x$(cat "${scheduler_pidfile}")" != "x"${pid} ] ; then
                log_message "Exit: Backup of ${host} at ${backup_level} level started by PID $(cat "${scheduler_pidfile}"), exiting"
                exit 0
            fi

            # shellcheck disable=SC2064
            trap "clean_up ${backup_level} ${host}" EXIT

            if [ -n "${lockfile[*]}" ] && [ -r "${lockfile[*]}" ] ; then
                if kill -0 "$(<"${lockfile[*]}")" > /dev/null 2>&1 ; then
                    while [ -r "${lockfile[*]}" ] ; do
                        sleep $(( ( RANDOM % scheduler_run_max_delay ) + 1))m
                    done
                fi
            fi

            if [ -n "${sync_first[*]}" ] && [ "${retain_name[*]}" == "${retain_first[*]}" ] ; then
                log_message "Starting sync for ${backup_level} backup of ${host}"
                rsnapshot -c "${config}" sync
            fi
            if [ -n "${sync_first[*]}" ] && [ "${retain_name[*]}" == "${retain_first[*]}" ] ; then
                log_message "Rotating ${backup_level} snapshot of ${host}"
            else
                log_message "Starting ${backup_level} backup of ${host}"
            fi
            rsnapshot -c "${config}" "${backup_level}"
        else
            log_message "Error: Specified backup level '${backup_level}' is not configured in ${config}"
            exit 1
        fi

    else

        log_message "Error: Config file ${config} not found"
        exit 1

    fi
}

prepare_backup () {

    local backup_level="${1}"
    local host="${2}"

    local scheduler_pidfile
    scheduler_pidfile="${scheduler_lock_dir}/$(basename "${script}")-${backup_level}-${host}.pid"

    if [ -r "${scheduler_config_dir}/${host}/rsnapshot.conf" ] ; then
        local config="${scheduler_config_dir}/${host}/rsnapshot.conf"
    else
        log_message "Error: configuration for ${host} not found"
        return
    fi

    if [ -r "${scheduler_config_dir}/${host}/stop" ] || [ -r "${scheduler_config_dir}/${host}/disable" ] || [ -r "${scheduler_config_dir}/${host}/disabled" ] ; then
        log_message "Exit: backup of ${host} is disabled"
        return
    fi

    if [ -r "${config}" ] ; then

        local retain_name
        read -r -a retain_name <<< "$(grep -E "^retain\\s+${backup_level}\\s+" "${config}" | awk '{print $2}' | head -n 1)"

        if [ "${retain_name[*]}" == "${backup_level}" ] ; then

            if [ -f "${scheduler_pidfile}" ] ; then
                log_message "Backup of ${host} at ${backup_level} level already in progress, managed by PID $(cat "${scheduler_pidfile}")"
                return
            fi

            # shellcheck disable=SC2174
            test -d "$(dirname "${scheduler_pidfile}")" || mkdir -p -m 755 "$(dirname "${scheduler_pidfile}")"
            echo ${pid} > "${scheduler_pidfile}"
            sleep 1

            if [ "x$(cat "${scheduler_pidfile}")" != "x"${pid} ] ; then
                log_message "Backup of ${host} at ${backup_level} level started by PID $(cat "${scheduler_pidfile}")"
                return
            fi

            if [ "${scheduler_type}" == "batch" ] ; then

                if type batch > /dev/null 2>&1 ; then
                    log_message "Scheduling ${backup_level} backup of ${host} using batch"
                    echo "${script} batchrun ${backup_level} ${host}" | ${scheduler_batch_command} > /dev/null 2>&1
                else
                    log_message "Scheduling ${backup_level} backup of ${host}"
                    (${script} delayrun "${backup_level}" "${host}") &
                fi

            elif [ "${scheduler_type}" == "sleep" ] ; then

                log_message "Scheduling ${backup_level} backup of ${host}"
                (${script} delayrun "${backup_level}" "${host}") &

            fi

        fi

    fi
}

schedule_backup () {

    local backup_level="${1}"
    local host="${2}"

    if [ "${host}" != "all" ] ; then

        prepare_backup "${backup_level}" "${host}"

    elif [ "${host}" == "all" ] ; then
        local random_hosts
        read -r -a random_hosts <<< "$(find "${scheduler_config_dir}" -maxdepth 1 -mindepth 1 -type d -printf "%f\\n" | sort -R | xargs)"
        for name in "${random_hosts[@]}" ; do
            prepare_backup "${backup_level}" "$(basename "${name}")"
        done
    fi

}

case ${arg_mode} in
    run)         run_backup "${arg_backup_level}" "${arg_host}" ;;
    batchrun)    wait_for_pid "${arg_backup_level}" "${arg_host}" ; run_backup "${arg_backup_level}" "${arg_host}" ;;
    delayrun)
                 # shellcheck disable=SC2119
                 delay_backup ; run_backup "${arg_backup_level}" "${arg_host}" ;;
    schedule)    schedule_backup "${arg_backup_level}" "${arg_host}" ;;
    *)           display_usage && exit 0 ;;
esac

