#!/bin/bash

# Original author: Pim van Pelt <pim@bit.nl>
# Taken from OSS publication upstream:
# debit.bit.nl:/debian/dists/stable/main/binary-i386/bit-cron-1.9-20060627.02.deb
# and modified for use at Google.
#
# Author: Pim van Pelt <pim@google.com>

# This is the master cron file format. It is designed to be quiet
# and not shout anything at all to stdout/stderr in normal use.
#
# Usage: bitcron [-n] /path/to/crondefinition.cron [arg [arg ..]]
#
# This program eats either one or two arguments from the commandline.
# If -n is given as the first argument, output is sent to stdout rather
# than MAILTO or ESCALATE_MAILTO (this is a `dryrun'). The next argument
# is a filename consisting of the cron definition. The file is sourced
# and executed, but within a framework that makes crons much easier to
# live with. You SHOULD have the filename of your crondefinition end
# in ".cron", so it is apparent to other people that this is indeed
# a cron definition. All other arguments are passed to the crontab's
# main function, called bitcron_main; see below.
#
# The cron definition file consists of the following shell variables:
#
# PATH: Optional. You can set a standard path for your cron to search
#   programs in. Note that cron sets a minimal path, so you probably need
#   this if you are going to call programs from weird places.
# Example: PATH=$PATH
#
# AUTHOR: Mandatory. Your full name, so we can track the author in case
#   something goes horribly wrong.
# Example: AUTHOR="Pim van Pelt"
#
# NAME: Mandatory. The name of your script. This MUST be a terse
#   descriptive name consisting of [A-Za-z0-9\-\_], and in particular
#   no whitespace. It's used in logs and mails.
# Example: NAME="example"
#
# MASTERLOG: Optional. If this is set, the transcript of the log is copied
#   to this file upon completion. If it is not set, the transcript is silently
#   deleted. Please make sure that this filename is writable for the calling
#   user, and that its name reveals who wrote it (including $NAME is a good
#   idea.
# Example: MASTERLOG="/var/log/cron-${NAME}.log"
#
# MAILTO: Optional. If this is set, the transcript of the log is mailed to
#   this user using /usr/sbin/sendmail -t, in the event of no warnings,
#   errors, or fatal errors, aka 'if all went well'. If it is not set, the
#   mail is not sent on succesful completion of this cronjob. Please use
#   with care if your cron runs often.
# Example: MAILTO="pim+noise@google.com"
#
# ESCALATE_MAILTO: Mandatory. If this is set, the transcript of the log
#   is mailed to this user using /usr/sbin/sendmail -t, in the event of
#   any warning, error or fatal error, aka 'if anything borked'. Please
#   make sure that this address is not a single person, but rather a role
#   account. This ensures better closure and continuity.
# Example: ESCALATE_MAILTO="maps-alerts@google.com"
#
# Variables your script MUST NOT change are:
# ERR, WRN, HOSTNAME, FATAL, FQDN
#
# Your cron-definition does not shebang. It begins with these variables.
# It then has at least one function called bitcron_main(), in which you write
# your cron just like you normally would, but each step is prepended by a
# short oneliner describing what you are doing and appended with a line
# stating you are done doing it, followed by an empty line.
#
# In your bitcron_main() script, you have three functions at your disposal:
# warning $TEXT
# error $TEXT
# fatal $TEXT
#
# where warning() prints the TEXT and increments the warning counter, error()
# increments the error counter and fatal() increments the error counter and
# immediately halts execution of the cron.
#
# For example:
# bitcron_main()
# {
# echo "Running rsync"
# rsync blabla || warning "Could not rsync"
# echo "Done (rsync)"
# echo ""
#
# echo "Doing something crucial"
# /usr/local/bin/crucial.sh blabla || \
#   fatal "Could not do the crucial stuff, aborting!"
# echo "Done (crucial)"
# echo ""
#
# echo "Succesfully executed the cron"
# }
#
# In the event of a fatal error (retval of crucial.sh != 0), the line
# 'echo "Done (crucial)"' will not be executed, rather the cronscript
# stops and mails to the escalation mail address that there was a serious
# error which needs human intervention.
#
# Return values of bitcron depend on the presence of errors or warnings.
# If there are no errors nor warnings, 0 is returned to the calling shell.
# If there are errors, 127 is returned. If there are no errors, but there
# are warnings, 126 is returned. If you somewhere set the RETVAL variable
# (which is optional), that value is returned instead.
#
# Please note that your bitcron_main() function MUST NOT use exit. This messes up
# the clean execution of this script, prohibiting it from mailing or
# returning a decent exitcode to the calling shell.
#

# BITCRON_SEARCHPATH="/usr/local/cronscripts/ /etc/cronscripts/ ./"
BITCRON_SEARCHPATH="~/cronscripts/ ./"
BITCRON_REPORTTOOL="/usr/sbin/sendmail -t"

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

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

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

function bitcron_report()
{
if [ $WRN -eq 0 -a $ERR -eq 0 ]; then
  if [ ! -z "$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-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 ""
  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"
  ) | ${BITCRON_REPORTTOOL}
  fi
else
  (
  echo "From: $HOSTNAME $NAME <`whoami`@$FQDN>"
  echo "To: $ESCALATE_MAILTO"
  if [ ! -z "$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-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 ""
  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"
  ) | ${BITCRON_REPORTTOOL}
fi
}

function bitcron_begin()
{
if [ -z "$MASTERLOG" ]; then
  LOG="/tmp/bitcron-$NAME.$$"
else
  LOG="${MASTERLOG}.$$"
fi
rm -f -- $LOG
exec 3>&1 4>&2 1>$LOG 2>&1
WRN=0
ERR=0
FATAL=0
FQDN=$(hostname)
HOSTNAME=$(hostname -s)
if [ "A`echo $FQDN | grep '\.'`" = "A" ]; then
  FQDN="${FQDN}.corp.google.com"
fi

echo "Beginning $NAME cronjob at `date`"
echo ""
}

function bitcron_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

bitcron_report

if [ ! -z "$MASTERLOG" ]; then
  cp -f -- "$LOG" "$MASTERLOG"
fi
rm -f -- $LOG >/dev/null 2>&1
if [ ! -z $RETVAL ]; then
  exit $RETVAL
elif [ $ERR -ne 0 ]; then
  exit 127
elif [ $WRN -ne 0 ]; then
  exit 126
fi
exit 0
}

function bitcron_usage()
{
  echo ""
  echo "Usage: bitcron [-n] /path/to/crondefinition.cron [arg [arg ..]]"
  echo ""
  echo "Please read the bitcron script. It has documentation at the"
  echo "top of the file. Look in $0 for more details."
  echo "-- Author: Pim van Pelt <pim@google.com>"
}

# Here goes!
if [ $# -lt 1 ]; then
  bitcron_usage
  exit 128
fi

# See if we are in dryrun mode.
if [ $1 = "-n" ]; then
  BITCRON_REPORTTOOL="/bin/cat"
  shift
fi

BITCRON_DEFINITION=$1
shift;
#
# Search through paths if the cron definition was not an absolute path.
#
if [ ${BITCRON_DEFINITION%%/*} ]; then
  for BITCRON_DIR in ${BITCRON_SEARCHPATH}; do
    if [ -r ${BITCRON_DIR}${BITCRON_DEFINITION} ]; then
      break;
    fi
  done
else
  BITCRON_DIR=""
fi

if [ ! -r ${BITCRON_DIR}${BITCRON_DEFINITION} ]; then
  echo "bitcron: Unreadable cron definition: ${BITCRON_DIR}${BITCRON_DEFINITION}"
  if [ ${BITCRON_DEFINITION%%/*} ]; then
    echo "I tried in: ${BITCRON_SEARCHPATH}"
  fi
  bitcron_usage
  exit 128
fi


. ${BITCRON_DIR}${BITCRON_DEFINITION}

if [ -z "$NAME" ]; then
  echo "bitcron: Missing mandatory variable NAME in ${BITCRON_DIR}${BITCRON_DEFINITION}"
  bitcron_usage
  exit 128
fi
if [ -z "$AUTHOR" ]; then
  echo "bitcron: Missing mandatory variable AUTHOR in ${BITCRON_DIR}${BITCRON_DEFINITION}"
  bitcron_usage
  exit 128
fi
if [ -z "$ESCALATE_MAILTO" ]; then
  echo "bitcron: Missing mandatory variable ESCALATE_MAILTO in ${BITCRON_DIR}${BITCRON_DEFINITION}"
  bitcron_usage
  exit 128
fi

bitcron_begin
bitcron_main $*
bitcron_end