commit e3264e6489a4136d6579d2c8e1197af848c12d31 Author: Pim van Pelt Date: Sun Jul 13 23:39:04 2025 +0200 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21e59e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Debian packaging artifacts +debian/.debhelper/ +debian/.gocache/ +debian/files +debian/bitcron +debian/*.substvars +debian/debhelper-build-stamp +debian/*.debhelper diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f7a29ff --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +#!/usr/bin/make -f + +.PHONY: pkg-deb deb-clean + +pkg-deb: + dpkg-buildpackage -us -uc + +clean: + rm -f ../bitcron_*.deb ../bitcron_*.changes ../bitcron_*.tar.xz ../bitcron_*.dsc ../bitcron_*.buildinfo + rm -rf debian/.debhelper debian/files debian/bitcron debian/*.substvars debian/debhelper-build-stamp diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..1ba6fac --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +bitcron (1.0-1) unstable; urgency=medium + + * Initial release. + + -- Pim van Pelt Sun, 13 Jul 2025 12:00:00 +0000 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..2cee894 --- /dev/null +++ b/debian/control @@ -0,0 +1,22 @@ +Source: bitcron +Section: admin +Priority: optional +Maintainer: Pim van Pelt +Build-Depends: debhelper-compat (= 13) +Standards-Version: 4.6.0 +Homepage: https://git.ipng.ch/ipng/bitcron +Vcs-Git: https://git.ipng.ch/ipng/bitcron.git +Vcs-Browser: https://git.ipng.ch/ipng/bitcron + +Package: bitcron +Architecture: all +Depends: ${misc:Depends}, bash, sendmail | mail-transport-agent +Description: Enhanced cron script framework with error handling and reporting + Bitcron is a framework for writing cron scripts that provides enhanced + error handling, warning management, and automatic email reporting capabilities. + It makes cron jobs more robust and easier to monitor by providing structured + logging and escalation mechanisms. + . + The framework includes functions for warning(), error(), and fatal() reporting, + automatic log generation, and configurable email notifications for both + successful runs and error conditions. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..612ee92 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,25 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: bitcron +Upstream-Contact: Pim van Pelt +Source: https://github.com/pimvanpelt/bitcron + +Files: * +Copyright: Pim van Pelt +License: GPL-2+ + +License: GPL-2+ + This package 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; either version 2 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". \ No newline at end of file diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..3bdde4a --- /dev/null +++ b/debian/install @@ -0,0 +1,2 @@ +src/bitcron usr/bin +etc/example.cron etc/bitcron \ No newline at end of file diff --git a/debian/postinst b/debian/postinst new file mode 100755 index 0000000..0c68e56 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +case "$1" in + configure) + # Create the log directory for bitcron + mkdir -p /var/log/bitcron + chmod 755 /var/log/bitcron + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..f50daeb --- /dev/null +++ b/debian/rules @@ -0,0 +1,8 @@ +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_auto_clean: +override_dh_auto_build: +override_dh_auto_configure: \ No newline at end of file diff --git a/etc/example.cron b/etc/example.cron new file mode 100644 index 0000000..ae85cc7 --- /dev/null +++ b/etc/example.cron @@ -0,0 +1,26 @@ +NAME="mycron" +AUTHOR="Pim van Pelt" +MAILTO="pim@ipng.ch" +ESCALATE_MAILTO="pim+escalate@ipng.ch" +MASTERLOG="/var/log/bitcron/${NAME}.log" +LINT='$Id$' + +# You can test this thing with: +# ./bitcron -n example.cron foo bar baz +bitcron_main() +{ + echo "Version: $LINT" + if [ $# -ne 0 ]; then + echo "Your commandline arguments were:" + while [ $# -gt 0 ]; do echo $1; shift; done + fi + + warning "You beter watch it .." + + error "Oops, now you blew it!" + + fatal "I refuse to work with you any longer!!!" + + // Not reached + echo "Not reached, because of a fatal error" +} diff --git a/src/bitcron b/src/bitcron new file mode 100755 index 0000000..6bca3e7 --- /dev/null +++ b/src/bitcron @@ -0,0 +1,326 @@ +#!/usr/bin/env bash +# Author: Pim van Pelt + +# 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@ipng.nl" +# +# 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="pim@ipng.nl" +# +# 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="./ ~/bitcron/ /etc/bitcron/ /usr/local/etc/bitcron/" +BITCRON_REPORTTOOL="/usr/sbin/sendmail -t" + +fatal() +{ +echo "" +echo "===== FATAL =====" +echo $1 +echo "===== FATAL =====" +ERR=$(($ERR + 1)) +FATAL=1; +bitcron_end +} + +error() +{ +echo "" +echo "===== ERROR =====" +echo $1 +echo "===== ERROR =====" +ERR=$(($ERR + 1)) +} + +warning() +{ +echo "" +echo "===== WARNING =====" +echo $1 +echo "===== WARNING =====" +WRN=$(($WRN + 1)) +} + +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 +} + +bitcron_begin() +{ +if [ -z "$MASTERLOG" ]; then + LOG="/tmp/bitcron-$NAME.$$" +else + LOG="${MASTERLOG}.$$" +fi +mkdir -p $(dirname $LOG) +rm -f -- $LOG +exec 3>&1 4>&2 1>$LOG 2>&1 +WRN=0 +ERR=0 +FATAL=0 +FQDN=$(hostname) +HOSTNAME=$(hostname -s) +if [ `uname -s` = "Linux" ]; then + HOSTNAME=$(hostname -s) + FQDN=$(hostname -f) +fi +if [ "A`echo $FQDN | grep '\.'`" = "A" ]; then + FQDN="${FQDN}.paphosting.net" +fi + +echo "Beginning $NAME cronjob at `date`" +echo "" +} + +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 + mv -f -- "$LOG" "$MASTERLOG" +fi +if [ ! -z $RETVAL ]; then + exit $RETVAL +elif [ $ERR -ne 0 ]; then + exit 127 +elif [ $WRN -ne 0 ]; then + exit 126 +fi +exit 0 +} + +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 " +} + +# 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