#!/bin/bash
# /usr/lib/aep-cron/aep-cron-run -- VORSCHLAG (noch nicht paketiert)
#
# Faehrt einen Cron-Job so, dass seine Ausgabe in Graylog landet, sein Ergebnis
# messbar ist und zwei Laeufe sich nicht ueberholen.
#
#   aep-cron-run <jobname> -- <kommando> [argumente...]
#
# WARUM: Cron schickt Ausgabe per Mail. Auf unseren Knoten ist MAILTO leer, also
# verschwindet sie -- und ein Job, der scheitert, tut das unbemerkt. syslog ist der
# Weg nach Graylog (Regel: Graylog ist erste Wahl fuer Dienstausgaben), und ein
# Zustandsfile macht Erfolg und Alter fuer Zabbix messbar.
#
# WARUM bash und nicht sh: die Trennung von stdout und stderr in zwei logger-
# Prozesse braucht Process Substitution (>(...)), und die gibt es in POSIX sh
# nicht. Dasselbe Muster steht in keepalived-notify.sh.
#
# WAS DIESER WRAPPER NICHT KANN -- bitte vor dem Einhaengen lesen:
#
# Ein Job, der seine Deskriptoren SELBST umleitet, ist fuer jeden Wrapper
# unsichtbar. Wer ihn wrappt und den Erfolg am leeren stderr abliest, haelt einen
# blinden Filter fuer stillen Betrieb.
#
# Gemessen auf tick1 (Graylog-Mengengeruest, 24.09.2026): check_service.sh macht
# 2880 Ausfuehrungen am Tag, also 68 % aller Cron-Laeufe, und leitet in Zeile 29
# selbst per 'exec > >(logger ...)' um. Seine Fehler stehen heute schon als
# app=check_service in Graylog -- dieser Job gehoert NICHT gewrappt, der Wrapper
# saehe nichts und wuerde eine Luecke als Ruhe melden.
#
# Dasselbe gilt fuer Crontab-Zeilen mit '> /dev/null 2>&1': was dorthin geht,
# sieht der Wrapper nicht. Er erkennt das beim Start und sagt es (Meldung unten),
# damit der Irrtum auffaellt, statt still zu bleiben. Zwei solche Zeilen gibt es
# auf tick1 heute.
#
# Wrappersichtbar bleiben nach diesem Geruest rund 1332 Ausfuehrungen am Tag, mit
# einer heutigen stderr-Menge von 0 -- diese Null ist der Grund fuer den Wrapper
# und nicht sein Erfolgsnachweis.
set -u

PROG=${0##*/}
STATE_DIR=${AEP_CRON_STATE_DIR:-/var/lib/aep-cron}
LOCK_DIR=${AEP_CRON_LOCK_DIR:-/run/lock}
FACILITY=${AEP_CRON_FACILITY:-cron}
LOG_SOCKET=${AEP_CRON_LOG_SOCKET:-/dev/log}

die() { echo "$PROG: $*" >&2; exit 78; }   # 78 = EX_CONFIG

job=${1-}
case $job in
'' | -h | --help) die "Aufruf: $PROG <jobname> -- <kommando> [argumente...]" ;;
*[!A-Za-z0-9_.-]*) die "ungueltiger Jobname '$job' (erlaubt: A-Za-z0-9_.-)" ;;
esac
shift
[ "${1-}" = "--" ] || die "nach dem Jobnamen fehlt '--'"
shift
[ $# -gt 0 ] || die "kein Kommando angegeben"

# Tag-Form mit SCHRAEGSTRICH, nicht mit Punkt -- gemessen entschieden am
# 24.09.2026: das Graylog-Feld 'tag' ist ein keyword-Feld (der Wert wird roh
# gespeichert, nicht zerlegt), und der Bestand ist voll von Schraegstrich-Tags:
# postfix/qmgr allein mit Millionen Dokumenten. Der Schraegstrich ist im
# syslog-Tag die etablierte Konvention fuer "Programm/Unterkomponente". Eine
# eigene Form (aep-cron.<job>) einzufuehren, um in Lucene ein Escaping zu
# sparen, brachte zwei Muster in denselben Bestand -- das kostet mehr als das
# Escaping. Suche in Graylog: tag:"aep-cron/<job>" (mit Anfuehrungszeichen).
tag="aep-cron/$job"
state="$STATE_DIR/$job"
lock="$LOCK_DIR/aep-cron-$job.lock"

# LAUT SCHEITERN, wenn der Weg nach syslog fehlt. Sonst laeuft der Job und seine
# Ausgabe geht ins Nichts -- genau die stille Sorte Fehler, die wir diese Woche
# mehrfach hatten. Der Zustand wird trotzdem geschrieben, damit Zabbix es sieht,
# auch wenn niemand die Mail liest (MAILTO ist leer).
if [ ! -S "$LOG_SOCKET" ]; then
	mkdir -p "$STATE_DIR" 2>/dev/null &&
		printf 'rc=78 ts=%s dur=0 note=no-syslog\n' "$(date -u +%s)" > "$state" 2>/dev/null
	die "kein syslog-Socket unter $LOG_SOCKET -- Ausgabe waere unauffindbar, Job nicht gestartet"
fi

command -v logger >/dev/null 2>&1 || die "logger fehlt (Paket bsdutils/util-linux)"
mkdir -p "$STATE_DIR" || die "kann $STATE_DIR nicht anlegen"
mkdir -p "$LOCK_DIR" 2>/dev/null || true

# Ueberlappung verhindern: ein Job, der laenger laeuft als sein Intervall, darf sich
# nicht selbst ueberholen. Kein Fehler, nur eine Meldung -- das ist Normalbetrieb
# bei einem langsamen Lauf, kein Alarm.
if command -v flock >/dev/null 2>&1; then
	exec 9>"$lock" || die "kann $lock nicht anlegen"
	if ! flock -n 9; then
		logger -p "$FACILITY.info" -t "$tag" "uebersprungen: ein Lauf ist noch aktiv"
		exit 0
	fi
fi

# Eine Ausgabeumleitung im Kommando blendet genau das aus, was hier eingesammelt
# werden soll. Das ist kein Fehler -- es kann gewollt sein --, aber es muss
# SICHTBAR sein, sonst liest jemand spaeter die Stille als Erfolg.
case " $* " in
*'>/dev/null'* | *'> /dev/null'* | *'2>&1'* | *' > '* | *' >> '* | *' 2> '* | *' &> '*)
	logger -p "$FACILITY.notice" -t "$tag" \
		"Hinweis: das Kommando enthaelt eine Ausgabeumleitung -- was dorthin geht, sieht dieser Wrapper nicht"
	;;
esac

start=$(date -u +%s)

# stdout und stderr getrennt nach syslog. --id=$$ haengt die PID an, damit sich
# parallele Jobs in Graylog auseinanderhalten lassen.
exec > >(logger -p "$FACILITY.info" -t "$tag" --id=$$) \
	2> >(logger -p "$FACILITY.err" -t "$tag" --id=$$)

rc=0
"$@" || rc=$?

dur=$(( $(date -u +%s) - start ))

# FALLSTRICK, der hier absichtlich behandelt wird: die beiden logger laufen als
# Kindprozesse an Pipes. Wer einfach 'exit' aufruft, verliert die letzten Zeilen,
# weil der Prozess weg ist, bevor logger gelesen hat. Deshalb werden die
# Deskriptoren geschlossen und auf die Kinder gewartet.
exec 1>&- 2>&-
wait

umask 022
tmp=$(mktemp "$state.XXXXXX") || exit "$rc"
printf 'rc=%s ts=%s dur=%s\n' "$rc" "$(date -u +%s)" "$dur" > "$tmp"
chmod 0644 "$tmp"
mv -f "$tmp" "$state"

exit "$rc"
