#!/bin/bash
#
# Get output of backup program and output statistics
# for check_mk lnx_backup plugin
#
# (c) 2017-2026 Marcel Pennewiss <opensource@pennewiss.de>
#
# Version: 1.3.1
# Last-Modified: 2026-05-12
#
# USAGE:
#   Start lnx_backup without any parameter to show usage/help.
#
# REQUIREMENTS:
#   * check_mk
#   * mktemp
#   * stdbuf
#
# This is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  This file is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

usage() {

    echo "Usage: lnx_backup IDENT TYPE CMD [ARGS...]"
    echo ""
    echo "Execute PROGRAM as subprocess while getting statistic information"
    echo "about the running backup process from subprocess output and write"
    echo "it to an output file. This file can be monitored using Check_MK."
    echo "The Check_MK Agent will forward the information of all backup files"
    echo "to the monitoring server."
    echo "The script outputs STDOUT and STDERR like running without scripts!"
    echo ""
    echo "IDENT   - Name of the backup (e.g. My_rsync_backup)"
    echo "TYPE    - Used tool or function (allowed: duply, rsync, tar, refresh, empty)"
    echo "          \"refresh\" will just refresh the start/end of backup information"
    echo "          \"empty\" will just create backup information with zero values"
    echo "CMD     - Program to run (e.g. rsync)"
    echo "ARGS    - All arguments (e.g. --stats --archive /source /backup)"
    echo ""
    echo "rsync requires \"--stats\" as argument to output statistic information"
    echo "which will be fetched by this script. Any PROGRAM should not be run with"
    echo "quiet option!"

}

create_temp_files() {

    # create temporary files for logging stdout and stderr
    STDOUT_TEMP=$(mktemp -t mkbackup_stdout_XXXXXXXX)
    STDERR_TEMP=$(mktemp -t mkbackup_stderr_XXXXXXXX)

}

cleanup_temp_files() {

    # cleanup temporary files
    rm -f ${STDOUT_TEMP} 2>/dev/null
    rm -f ${STDERR_TEMP} 2>/dev/null

}

prepare() {

    # set default values
    OUTPUT_EXIT_CODE=-1
    OUTPUT_SOURCE_FILES=0
    OUTPUT_SOURCE_FILESIZE=0
    OUTPUT_NEW_FILES=0
    OUTPUT_NEW_FILESIZE=0
    OUTPUT_DELETED_FILES=0
    OUTPUT_CHANGED_FILES=0
    OUTPUT_CHANGED_FILESIZE=0
    OUTPUT_BACKUP_SIZE=0
    OUTPUT_ERRORS=0

    # create output path if it does not exist
    if [ ! -d "${OUTPUT_PATH}" ]; then
        mkdir -p "${OUTPUT_PATH}"
    fi

    # output files with path
    OUTPUT_FILE="${OUTPUT_PATH}/${IDENT}"
    OUTPUT_TMP_FILE="${OUTPUT_PATH}/.${IDENT}.running"

}

run_process() {

    # check for known backup type
    case ${TYPE} in
        duply);;
        tar);;
        rsync);;
        refresh);;
        empty);;
        *)  echo -e "ERROR: Unknown backup program ${TYPE}.\n" >&2
            usage >&2
            exit 1
            ;;
    esac

    # run process for all backup types except refresh or empty
    if [ "${TYPE}" != "refresh" ] && [ "${TYPE}" != "empty" ]; then

        if ! type $1 >/dev/null 2>&1; then
            echo -e "ERROR: Cannot run $1. Command not found.\n" >&2
            usage >&2
            exit 1
        fi

        # run wrapped program with arguments and get return code
        OUTPUT_START_TIME=$(date +%s)
        stdbuf -e 0 -o 0 $@ > >(tee ${STDOUT_TEMP}) 2> >(tee ${STDERR_TEMP} >&2)
        OUTPUT_EXIT_CODE=$?
        OUTPUT_END_TIME=$(date +%s)
        [ -n ${OUTPUT_START_TIME} ] && [ ${OUTPUT_START_TIME} -eq ${OUTPUT_END_TIME} ] && OUTPUT_END_TIME=$((OUTPUT_START_TIME + 1))

    fi

    # post process output
    post_process_${TYPE}

    # create backup information file
    save_information
}

# shellcheck disable=SC2329
post_process_duply() {

    if [ ${OUTPUT_EXIT_CODE} -ne 0 ]; then
      return
    fi

    local STATS=$(cat ${STDOUT_TEMP} | sed -n '/^.*\[ Backup Statistics \].*$/,/^-------------------------------------------------$/{x;/^$/!p;}')

    OUTPUT_SOURCE_FILES=$(echo "${STATS}" | sed -ne 's/^SourceFiles\ \([0-9]*\).*/\1/p')
    OUTPUT_SOURCE_FILESIZE=$(echo "${STATS}" | sed -ne 's/^SourceFileSize\ \([0-9]*\).*/\1/p')
    OUTPUT_NEW_FILES=$(echo "${STATS}" | sed -ne 's/^NewFiles\ \([0-9]*\).*/\1/p')
    OUTPUT_NEW_FILESIZE=$(echo "${STATS}" | sed -ne 's/^NewFileSize\ \([0-9]*\).*/\1/p')
    OUTPUT_DELETED_FILES=$(echo "${STATS}" | sed -ne 's/^DeletedFiles\ \([0-9]*\).*/\1/p')
    OUTPUT_CHANGED_FILES=$(echo "${STATS}" | sed -ne 's/^ChangedFiles\ \([0-9]*\).*/\1/p')
    OUTPUT_CHANGED_FILESIZE=$(echo "${STATS}" | sed -ne 's/^ChangedFileSize\ \([0-9]*\).*/\1/p')
    OUTPUT_BACKUP_SIZE=$(echo "${STATS}" | sed -ne 's/^TotalDestinationSizeChange\ \([0-9]*\).*/\1/p')
    OUTPUT_ERRORS=$(echo "${STATS}" | sed -ne 's/^Errors\ \([0-9]*\).*/\1/p')

}

# shellcheck disable=SC2329
post_process_rsync() {

    if [ ${OUTPUT_EXIT_CODE} -ne 0 ]; then
      return
    fi

    local STATS=$(cat ${STDOUT_TEMP} | sed -n '/^Number of files: [0-9]*.*$/,/^total size is$/p')

    OUTPUT_SOURCE_FILES=$(echo "${STATS}" | sed -ne 's/^Number of files:\ \([0-9,]*\).*/\1/p' | sed -e 's/\,//g')
    OUTPUT_SOURCE_FILESIZE=$(echo "${STATS}" | sed -ne 's/^Total file size:\ \([0-9,]*\).*/\1/p' | sed -e 's/\,//g')
    OUTPUT_NEW_FILES=$(echo "${STATS}" | sed -ne 's/^Number \(of\|of regular\) files transferred:\ \([0-9,]*\).*/\2/p' | sed -e 's/\,//g')
    OUTPUT_NEW_FILESIZE=$(echo "${STATS}" | sed -ne 's/^Total transferred file size:\ \([0-9,]*\).*/\1/p' | sed -e 's/\,//g')
    OUTPUT_BACKUP_SIZE=$(echo "${STATS}" | sed -ne 's/^Literal data:\ \([0-9,]*\).*/\1/p' | sed -e 's/\,//g')

}

# shellcheck disable=SC2329
post_process_tar() {

    if [ ${OUTPUT_EXIT_CODE} -ge 2 ]; then
      return
    fi

    OUTPUT_SOURCE_FILES=$(cat ${STDOUT_TEMP} | wc -l)
    OUTPUT_NEW_FILES=${OUTPUT_SOURCE_FILES}
    OUTPUT_BACKUP_SIZE=$(cat "${STDERR_TEMP}" | sed -ne 's/^\(Total\ bytes\ written\|Gesamtzahl\ geschriebener\ Bytes\):\ \([0-9]*\).*/\2/p')
    OUTPUT_ERRORS=$(cat ${STDERR_TEMP} | grep -ve '^tar:\ \(Removing\ leading\|Entferne f\)' | sed -n '1,/^\(Total\ bytes|Gesamtzahl\ geschriebener\ Bytes\)/{x;/^$/!p;}' | wc -l)

}

# shellcheck disable=SC2329
post_process_refresh() {

    if [ ! -f "${OUTPUT_FILE}" ];then
        echo "Last backup information in ${OUTPUT_FILE} not found." >&2
        exit 1
    fi

    OUTPUT_START_TIME=$(cat "${OUTPUT_FILE}" | sed -ne 's/^start_time\ \([0-9]*\).*/\1/p')
    OUTPUT_START_TIME=$((OUTPUT_START_TIME + 86400))
    OUTPUT_END_TIME=$(cat "${OUTPUT_FILE}" | sed -ne 's/^end_time\ \([0-9]*\).*/\1/p')
    OUTPUT_END_TIME=$((OUTPUT_END_TIME + 86400))
    OUTPUT_EXIT_CODE=$(cat "${OUTPUT_FILE}" | sed -ne 's/^exit_code\ \([0-9]*\).*/\1/p')
    OUTPUT_SOURCE_FILES=$(cat "${OUTPUT_FILE}" | sed -ne 's/^source_files\ \([0-9]*\).*/\1/p')
    OUTPUT_SOURCE_FILESIZE=$(cat "${OUTPUT_FILE}" | sed -ne 's/^source_filesize\ \([0-9]*\).*/\1/p')
    OUTPUT_NEW_FILES=$(cat "${OUTPUT_FILE}" | sed -ne 's/^new_files\ \([0-9]*\).*/\1/p')
    OUTPUT_NEW_FILESIZE=$(cat "${OUTPUT_FILE}" | sed -ne 's/^new_filesize\ \([0-9]*\).*/\1/p')
    OUTPUT_DELETED_FILES=$(cat "${OUTPUT_FILE}" | sed -ne 's/^deleted_files\ \([0-9]*\).*/\1/p')
    OUTPUT_CHANGED_FILES=$(cat "${OUTPUT_FILE}" | sed -ne 's/^changed_files\ \([0-9]*\).*/\1/p')
    OUTPUT_CHANGED_FILESIZE=$(cat "${OUTPUT_FILE}" | sed -ne 's/^changed_filesize\ \([0-9]*\).*/\1/p')
    OUTPUT_BACKUP_SIZE=$(cat "${OUTPUT_FILE}" | sed -ne 's/^backup_size\ \([0-9]*\).*/\1/p')
    OUTPUT_ERRORS=$(cat "${OUTPUT_FILE}" | sed -ne 's/^errors\ \([0-9]*\).*/\1/p')

}

# shellcheck disable=SC2329
post_process_empty() {

    OUTPUT_EXIT_CODE=0
    OUTPUT_START_TIME=$(date +%s)
    OUTPUT_END_TIME=$((OUTPUT_START_TIME + 1))

}

save_information() {

    # create temporary information file
    echo "start_time ${OUTPUT_START_TIME}" > "${OUTPUT_TMP_FILE}"
    echo "end_time ${OUTPUT_END_TIME}" >> "${OUTPUT_TMP_FILE}"
    echo "exit_code ${OUTPUT_EXIT_CODE}" >> "${OUTPUT_TMP_FILE}"
    echo "source_files ${OUTPUT_SOURCE_FILES}" >> "${OUTPUT_TMP_FILE}"
    echo "source_filesize ${OUTPUT_SOURCE_FILESIZE}" >> "${OUTPUT_TMP_FILE}"
    echo "new_files ${OUTPUT_NEW_FILES}" >> "${OUTPUT_TMP_FILE}"
    echo "new_filesize ${OUTPUT_NEW_FILESIZE}" >> "${OUTPUT_TMP_FILE}"
    echo "deleted_files ${OUTPUT_DELETED_FILES}" >> "${OUTPUT_TMP_FILE}"
    echo "changed_files ${OUTPUT_CHANGED_FILES}" >> "${OUTPUT_TMP_FILE}"
    echo "changed_filesize ${OUTPUT_CHANGED_FILESIZE}" >> "${OUTPUT_TMP_FILE}"
    echo "backup_size ${OUTPUT_BACKUP_SIZE}" >> "${OUTPUT_TMP_FILE}"
    echo "errors ${OUTPUT_ERRORS}" >> "${OUTPUT_TMP_FILE}"

    # move temporary information file to final destination
    mv "${OUTPUT_TMP_FILE}" "${OUTPUT_FILE}"

}

if ([ $# -eq 2 ] && [ "${2}" != "refresh" ] && [ "${2}" != "empty" ]) || [ $# -lt 3 ]; then
    usage >&2
    exit 1
fi

OUTPUT_PATH=/var/lib/check_mk_agent/lnx_backup
IDENT=${1}
TYPE=${2}
shift 2

prepare
create_temp_files
run_process $@
cleanup_temp_files

exit ${OUTPUT_EXIT_CODE}
