#!/bin/bash
#
# bitcron was once written by Pim van Pelt <pim@ipng.nl>
# modifications by BIT employees <support@bit.nl>
#
# See 'man 1 bitcron' for documentation.
#
# You MUST NOT edit this file.
# You should be editing the cron definition file.
#


BITCRON_SEARCHPATH="/usr/share/bit-cron/cronscripts/ /etc/cronscripts/ ./"


fatal()
{
    echo ""
    echo "===== FATAL ====="
    echo "$1"
    echo "===== FATAL ====="
    ERR=$((ERR + 1))
    FATAL=1;
    func_end
}


error()
{
    echo ""
    echo "===== ERROR ====="
    echo "$1"
    echo "===== ERROR ====="
    ERR=$((ERR + 1))
}


warning()
{
    echo ""
    echo "===== WARNING ====="
    echo "$1"
    echo "===== WARNING ====="
    WRN=$((WRN + 1))
}


func_sendmail()
{
    if [ $WRN -eq 0 ] && [ $ERR -eq 0 ]; then
        if [ -n "$MAILTO" ]; then
            (
                echo "From: $HOSTNAME $NAME <$(whoami)@$FQDN>"
                echo "To: $MAILTO"
                echo "Subject: bitcron $NAME - succesful"
                echo "Date: $(date '+%a, %d %b %Y %T %z')"
                echo "Errors-To: $MAILTO"
                echo "Reply-To: $MAILTO"
                echo "X-Mailer: bitcron 128.59"
                echo "X-bitcron: $NAME"
                echo "X-Precedence: automatic"
                echo ""
                echo "Dear reader,"
                echo ""
                echo "This is the $0 program on $HOSTNAME informing"
                echo "you I have succesfully executed the bitcron $NAME"
                echo "with arguments '$BITCRONSCRIPTARGS'."
                echo ""
                echo "The log that I have for this cronjob follows:"
                cat "$LOG"
                echo ""
                echo "This mail is purely FYI, user intervention is not required."
                echo ""
                echo "-- "
                echo "Cheers,"
                echo "    $HOSTNAME"
            ) | /usr/sbin/sendmail -t
        fi
    else
        (
            echo "From: $HOSTNAME $NAME <$(whoami)@$FQDN>"
            echo "To: $ESCALATE_MAILTO"
            if [ -n "$MAILTO" ]; then
                echo "Cc: $MAILTO"
            fi
            if [ $FATAL -eq 0 ]; then
                echo "Subject: bitcron $NAME - $ERR error(s), $WRN warning(s)"
            else
                echo "Subject: bitcron $NAME - FATAL - $ERR error(s), $WRN warning(s)"
            fi
            echo "Date: $(date '+%a, %d %b %Y %T %z')"
            echo "Errors-To: $ESCALATE_MAILTO"
            echo "Reply-To: $ESCALATE_MAILTO"
            echo "X-Mailer: bitcron 128.59"
            echo "X-Precedence: automatic"
            echo "X-bitcron: $NAME"
            echo "X-Errors: $ERR"
            echo "X-Warnings: $WRN"
            echo ""
            echo "Dear reader,"
            echo ""
            echo "This is the $0 program on $HOSTNAME informing"
            echo "you that there was a problem running the bitcron $NAME"
            echo "with arguments '$BITCRONSCRIPTARGS'."
            echo ""
            echo "The logfile that lead up to this:"
            cat "$LOG"
            echo ""
            echo "I hope you can deal with this situation ASAP."
            echo ""
            echo "-- "
            echo "Cheers,"
            echo "    $HOSTNAME"
        ) | /usr/sbin/sendmail -t
    fi
}


func_begin()
{
    if [ -z "$MASTERLOG" ]; then
        LOG="/tmp/bitcron-$NAME.TMP.$$"
    else
        LOG="${MASTERLOG}.TMP.$$"
    fi

    exec 3>&1 4>&2 1>"$LOG" 2>&1

    WRN=0
    ERR=0
    FATAL=0
    LOCKED=0
    FQDN=$(hostname -f)
    HOSTNAME=$(hostname -s)

    if [ "A$(echo "$FQDN" | grep '\.')" == "A" ]; then
        FQDN="${FQDN}.mail.colo.bit.nl"
    fi

    echo "Beginning $NAME cronjob at $(date)"

    # Optional concurrent process locking.
    if [ -n "$BITCRONLOCK" ]; then
        if [ -e "$BITCRONLOCK" ]; then
            PID=$(cat "$BITCRONLOCK")
            if kill -0 "$PID" >/dev/null 2>&1; then
                LOCKED=1

                dtnow=$(date +%s)
                dtthen=$(stat -c"%Y" "${BITCRONLOCK}")
                dtdiff=$((dtnow - dtthen))
                if [ $dtdiff -ge "$BITCRONLOCKLIMIT" ]; then
                    echo ""
                    echo "[!!]                                          [!!]"
                    echo "[!!]                                          [!!]"
                    echo "[!!] THIS JOB WAS LOCKED FOR $dtdiff SECONDS! [!!]"
                    echo "[!!]                                          [!!]"
                    echo "[!!]                                          [!!]"
                    echo ""
                fi

                if [ -n "$BITCRONLOCKSILENT" ]; then
                    ERR=0
                    WRN=0
                    FATAL=0
                    func_end
                else
                    fatal "$PID is still active. Stopping execution."
                fi
            else
                if [ -z "$PID" ]; then
                    echo ""
                    echo "Race? '$BITCRONLOCK' existed, but we read an empty PID."
                    echo "Assuming it's safe to continue..."
                    echo ""
                else
                    [ -z "$BITCRONLOCKSILENT" ] && warning "Stale lockfile found? Process $PID seems inactive. Resuming."
                fi
            fi
        fi
        echo "Concurrency locking enabled: $BITCRONLOCK"
        echo $$ > "$BITCRONLOCK"
    fi

    echo ""
}


func_end()
{
    echo ""
    echo "Done with $NAME cronjob at $(date)"
    echo ""
    echo "There were $ERR error(s) and $WRN warning(s)"
    if [ $FATAL -ne 0 ]; then
        echo "This script had a fatal error!"
    fi

    exec 1>&3 2>&4

    func_sendmail

    # Do not rotate logfiles if this proces quits due to
    # it being locked by another process running. The
    # func_sendmail call above uses $LOG (the .TMP file)
    # so emails still make sense.
    # This prevents bit-crons from messing up each other's
    # logfiles, or ending up with only logfiles stating
    # the job was locked.
    if [ -n "$MASTERLOG" ] && [ $LOCKED -eq 0 ]; then
        # Rotate logs.
        if [ -e "$MASTERLOG" ]; then
            I="$NO_FILES_LOGRETENTION"
            [ -e "$MASTERLOG".$((I - 1)) ] && rm -f -- "$MASTERLOG".$((I - 1))
            while [ "$I" -ge 2 ];
            do
                I=$((I - 1))
                [ -e "$MASTERLOG".$((I - 1)) ] && mv -f -- "$MASTERLOG".$((I - 1)) "$MASTERLOG".$I
            done
            [ -e "$MASTERLOG" ] && mv -f -- "$MASTERLOG" "$MASTERLOG.0"
        fi
        cp -f -- "$LOG" "$MASTERLOG"
    fi

    rm -f -- "$LOG" >/dev/null 2>&1

    # Do not remove the lockfile if this proces quits due to
    # it being locked by another process running. Stale lockfiles
    # are fixed by the locking-section in func_begin() above.
    if [ $LOCKED -eq 0 ]; then
        if [ -n "$BITCRONLOCK" ]; then
            rm -f -- "$BITCRONLOCK" >/dev/null 2>&1
        fi
    fi

    if [ -n "$RETVAL" ]; then
        exit "$RETVAL"
    elif [ $ERR -ne 0 ]; then
        exit 127
    elif [ $WRN -ne 0 ]; then
        exit 126
    fi

    exit 0
}


func_stdout_err()
{
    echo "bitcron: $1"
    echo ""
    echo "Usage:"
    echo " user@host:~\$ bitcron /path/to/my.cron"
    echo ""
    echo "Please read 'man 1 bitcron'."
    exit 128
}


# Here goes!
if [ $# -lt 1 ]; then
    func_stdout_err "Missing bitcron script"
fi

CRONDEFINITION=$1; shift;
if [ "$CRONDEFINITION" = "--insecure" ]; then
    CRONDEFINITION=$1; shift;
fi

BITCRONSCRIPTARGS=$*

#
# Inform the user that when the cron definition starts with './', this will only work in interactive mode from a later version onwards.
#
if [[ "${CRONDEFINITION}" == ./* ]]; then
    if [ -t 0 ]; then
        echo "Notice: Using bit-cron with ${CRONDEFINITION} will only work in interactive mode in the future. Please use an absolute path."
    fi
fi

#
# DEPRECATED FUNCTION, WILL BE DELETED IN A LATER VERSION
# Search through paths if the cron definition was not an absolute path.
#
DIR=""
DEPRECATED=0

if [ "${CRONDEFINITION%%/*}" ]; then
    for TDIR in $BITCRON_SEARCHPATH; do
        if [ -r "${TDIR}""${CRONDEFINITION}" ]; then
            DIR=$TDIR
            break;
        fi
    done
    DEPRECATED=1
fi

if [ ! -r "${DIR}""${CRONDEFINITION}" ]; then
    func_stdout_err "Unreadable cron definition: ${DIR}${CRONDEFINITION}"
fi

# Ignore MAILTO env set from cron.d/crontab definition.
# Bit-cron scripts can define MAILTO when they want to.
MAILTO=

# Force locale to C, as this is what most automations expect.
# Also, the output of bit-cron is English, so adhere to such date time formats.
LANG=C
LC_TIME=C
LC_MESSAGES=C

# Before loading the cron definition, check if we're privileged
# and then wether or not the cron definition is owned by root.
#
# This is a security hole, otherwise. The user could edit the
# cron definition and have it execute as root. If '--insecure'
# occurs somewhere in the cmdline, it forces this insecure behavior.
if ! grep -q -- "--insecure" /proc/$$/cmdline; then
    if [ "$(id -u)" == "0" ] || [ "$(id -ru)" == "0" ]; then
        # Check cron definition ownership
        CDU=$(stat -c "%u" "${DIR}${CRONDEFINITION}")
        CDG=$(stat -c "%g" "${DIR}${CRONDEFINITION}")
        if [ "$CDU" -ne 0 ] && [ "$CDG" -ne 0 ]; then
            echo "Refusing to run as root: '${DIR}${CRONDEFINITION}' is owned by $CDU:$CDG - Security risk!" >>/var/log/bit-cron.log
            func_stdout_err "Refusing to run as root: '${DIR}${CRONDEFINITION}' is owned by $CDU:$CDG - Security risk!"
        fi

        # Check group/other writeability
        WCL=$(find "${DIR}${CRONDEFINITION}" -perm /022 | wc -l)
        if [ "$WCL" -ne 0 ]; then
            echo "Refusing to run as root: '${DIR}${CRONDEFINITION}' is writable for group/others - Security risk!" >>/var/log/bit-cron.log
            func_stdout_err "Refusing to run as root: '${DIR}${CRONDEFINITION}' is writable for group/others - Security risk!"
        fi
    fi
fi

# Load the CRON definition file
. "${DIR}""${CRONDEFINITION}"

[ -z "$BITCRONLOCKLIMIT" ] && BITCRONLOCKLIMIT=86400
[ -z "$NAME" ] && func_stdout_err "bitcron: Missing mandatory variable NAME in ${DIR}${CRONDEFINITION}"
[ -z "$ESCALATE_MAILTO" ] && func_stdout_err "bitcron: Missing mandatory variable ESCALATE_MAILTO in ${DIR}${CRONDEFINITION}"

# The number of files retained in the /var/log/bit-cron directory defaults to 10
[ -z "$NO_FILES_LOGRETENTION" ] && NO_FILES_LOGRETENTION=10

func_begin
if [ ${DEPRECATED} == 1 ]; then
	warning "DEPRECATED: Please use an absolute path instead of ${CRONDEFINITION}. This will no longer work in a future version."
fi
func_cron "$@"
func_end
