#!/bin/sh
# DocumentId: $Id: cron-apt 2048 2005-04-20 19:58:28Z ola $
#
# Copyright (C) 2001-2005 Ola Lundqvist <opal@debian.org>
#
# This program 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 program 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Changes:
#	2005-04-20 Ola Lundqvist <opal@debian.org>
#		Modified refrain file system to make file name configurable.
#	2005-04-20 Marc Haber <mh+debian-bugs@zugschlus.de>
#		Added support for refrain file.
#	2004-12-25 Marc Sherman <msherman@projectile.ca>
#		Added configurable support for aptitude instead of apt-get.
#	2004-09-23 Ola Lundqvist <opal@debian.org>
#		Escaped changes file data so '/' can be used in action.d lines.
#	2004-09-19 Marc Haber <mh+debian-bugs@zugschlus.de>
#		Add warning if dotlockfile not found.
#	2004-09-12 Ola Lundqvist <opal@debian.org>
#		Modified patch from Marc Haber to avoid symlink attacks.
#		Also use nicer cleaning.
#	2004-09-12 Marc Haber <mh+debian-bugs@zugschlus.de>
#		Patch to have multiple cron-apt running at the same time
#		without messing up the changelog.
#	2004-09-07 Marc Haber <mh+debian-bugs@zugschlus.de>
#		Patch to achieve run-parts behaviour.
#	2004-08-13 Ola Lundqvist <opal@debian.org>
#		Bugfixes.
#		Made *ON=changes available to more than only mail.
#	2004-08-12 Ola Lundqvist <opal@debian.org>
#		Added support for mailon=changes.
#		Marc Haber <mh+debian-bugs@zugschlus.de> give me the patch
#		that I have modified.
#		Fixed some other bugs that was introduced by me.
#		Added support for syslogon.
#	2004-08-09 Ola Lundqvist <opal@debian.org>
#		Cleaned up some code and fixed a minor logging bug.
#		Better argument checking code. Can now handle more than
#		one option.
#		Changed from status passing from echo to file.
#		Fixed so that stdout argument really works.
#	2004-08-08 Ola Lundqvist <opal@debian.org>
#		Fixed variable bug.
#		Cleaned up the code.
#		Fixed output function framework so it works as expected.
#		Better option handling and local output option.
#	2004-07-22 Ola Lundqvist <opal@debian.org>
#		Made it more POSIX compliant by using /dev/random if the
#		RANDOM variable do not contain a value. Thanks to
#		Bob Proulx <bob@proulx.com> for the ida.
#		Also changed -a and -o to && and ||, as Bob suggested.
#	2003-07-10 Ola Lundqvist <opal@debian.org>
#		Always run even if there was an error last time is now the
#		default. Fix so that you can write RUNSLEEP=0 too.
#	2003-07-10 Ola Lundqvist <opal@debian.org>
#		Added runsleep configuration option.
#		Now cron-apt is aware of the DEBIAN_FRONTEND = noninteractive
#		ENVIRONMENT.
#	2002-04-07 Ola Lundqvist <opal@debian.org>
#		Added skipping of comment lines in action files.
#		Added exra config.d directory.
#		Rewrote the mail and log part to make it more flexible.
#		Christian Perrier <Christian.Perrier@onera.fr> was the
#		one that needed this flexibility.
#		Also added skipping of backup files (*~).
#	2002-03-05 Ola Lundqvist <opal@debian.org>
#		Made it complete so it can be released.
#	2002-03-03 Ola Lundqvist <opal@debian.org>
#		Wrote the beginning.
#	2002-04-24 Ola Lundqvist <opal@debian.org>
#		Added a default path. Thanks to Donovan Baarda
#		<abo@minkirri.apana.org.au> for pointing it out.

export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
UMASK_TIGHT="077"
UMASK_RELAXED="022"
umask $UMASK_TIGHT

############################# arguments #######################################

STDOUT=""
ALLCONFIGS=""

while [ -n "$*" ] ; do
    if [ "$1" = "--help" ] ; then
	echo "USAGE: cron-apt [-i] [-s] [configfiles]"
	exit 0
    elif [ "$1" = "-i" ] ; then
	RUNIMMEDIATELY="yes"
	shift
    elif [ "$1" = "-s" ] ; then
	DEBUG=always
	STDOUT=yes
	shift
    else
	CONFIG="$1"
    	ALLCONFIGS="$ALLCONFIGS $CONFIG"
	shift
    fi
done
if [ -z "$CONFIG" ] ; then
    CONFIG=/etc/cron-apt/config
    ALLCONFIGS="$CONFIG"
fi

############################## lib and tmp dirs ###############################

LIBDIR="/var/lib/cron-apt"
CONFIGDIRNAME="$(echo $CONFIG | sed 's|/|_-_|g')"
TMPDIR=$(mktemp -d -t cron-apt.XXXXXX)
if [ $? -ne 0 ]; then
    echo "Error: Can not create a safe temporary directory."
    exit 1
fi

INITLOG="$TMPDIR/initlog"
RUNERROR="$TMPDIR/runerror"
RUNSYSLOG="$TMPDIR/runsyslog"
RUNLOG="$TMPDIR/runlog"
RUNMAIL="$TMPDIR/runmail"
ACTIONERROR="$TMPDIR/actionerror"
ACTIONSYSLOG="$TMPDIR/actionsyslog"
ACTIONLOG="$TMPDIR/actionlog"
ACTIONMAIL="$TMPDIR/actionmail"
TEMP="$TMPDIR/temp"
MAIL="$TMPDIR/mail"
DIFF="$TMPDIR/difftemp"
FILTER="$TMPDIR/filtertemp"
STATUS="$TMPDIR/status"

LOCKFILE="$LIBDIR/lockfile"
MAILCHDIR="$LIBDIR/$CONFIGDIRNAME/mailchanges"
ERROR="$TMPDIR/$CONFIGDIRNAME-error"

############################## defaults #######################################

ACTIONDIR="/etc/cron-apt/action.d"
ACTIONCONFDIR="/etc/cron-apt/config.d"
MAILMSGDIR="/etc/cron-apt/mailmsg.d"
MAILONMSGSDIR="/etc/cron-apt/mailonmsgs"
SYSLOGONMSGSDIR="/etc/cron-apt/syslogonmsgs"
REFRAINFILE="/etc/cron-apt/refrain"
NOLOCKWARN=""
ERRORMSGDIR="/etc/cron-apt/errormsg.d"
SYSLOGMSGDIR="/etc/cron-apt/syslogmsg.d"
LOGMSGDIR="/etc/cron-apt/logmsg.d"
LOG="/var/log/cron-apt/log"
DIFFONCHANGES="prepend"
MAILTO="root"
# error, always, never
SYSLOGON="upgrade"
# error, always
MAILON="error"
# error, never
EXITON="error"
# verbose, error
DEBUG="verbose"
# general options
OPTIONS="-o quiet=1"
# do always run as default
DONTRUN=""
# Random sleep time before run
RUNSLEEP="3600"
# The command to use (can be aptitude instead)
APTCOMMAND="/usr/bin/apt-get"
# If FILTERCTRLM is "true", then any line containing ^M in the apt-get
# output will be filtered from log/mail/console output.  This is useful
# with aptitude, which does not currently support -qq (very quiet).
FILTERCTRLM="false"

export DEBIAN_FRONTEND="noninteractive"
export LANG="C"
export LC_ALL="C"

# Read configuration.
for cfg in $ALLCONFIGS; do
  if [ -f "$cfg" ] ; then
    . "$cfg"
  else
    echo >&2 "The config file $cfg does not exist."
  fi
done

if [ -t 0 ]; then
    RUNIMMEDIATELY="yes"
fi
if test "$RUNIMMEDIATELY" = "yes"; then
    RUNSLEEP=
fi

############################## functions ######################################

# Send the mail to the system administrator. Will only be sent if there is
# something to send. No arguments needed.
onexit() {
    if [ -f "$TEMP" ] ; then
	rm -f "$TEMP"
    fi
    if [ -f "$ACTIONLOG" ] ; then
	rm -f "$ACTIONLOG"
    fi
    if [ -f "$ACTIONSYSLOG" ] ; then
	rm -f "$ACTIONSYSLOG"
    fi
    if [ -f "$ACTIONMAIL" ] ; then
	rm -f "$ACTIONMAIL"
    fi
    if [ -f "$ACTIONERROR" ] ; then
	rm -f "$ACTIONERROR"
    fi
    if [ -f "$RUNLOG" ] ; then
	rm -f "$RUNLOG"
    fi
    if [ -f "$ERROR" ] ; then
	rm -f "$ERROR"
    fi
    if [ -f "$RUNSYSLOG" ] ; then
	rm -f "$RUNSYSLOG"
    fi
    if [ -f "$RUNERROR" ] ; then
	rm -f "$RUNERROR"
    fi
    if [ -f "$RUNMAIL" ] ; then
	rm -f "$RUNMAIL"
    fi
    if [ -f "$MAIL" ] ; then
	SUBJECT="CRON-APT completed on $(uname -n) [$CONFIG]"
	if [ -f "$ERROR" ] ; then
	    SUBJECT="CRON-APT error on $(uname -n) [$CONFIG]"
	fi
	mail -s "$SUBJECT" "$MAILTO" < "$MAIL"
	rm -f "$MAIL"
    fi
    if [ -f "$DIFF" ] ; then
	rm -f "$DIFF"
    fi
    if [ -f "$FILTER" ] ; then
    	rm -f "$FILTER"
    fi
    if [ -d "$TMPDIR" ] ; then
	rmdir "$TMPDIR"
    fi
}

# Do the actual msg transfer. This reduce some duplications.
# 1 CATFILE
# 2 TOFILE (if = syslog log to syslog)
# 3 TOSTDOUT ()
tomsg() {
    TCAT="$1"
    TFILE="$2"
    TSTD="$3"
    if [ -n "$TCAT" ] && [ -r "$TCAT" ] ; then
	if [ -n "$TSTD" ] ; then
	    cat "$TCAT"
	fi
	if [ "$TFILE" != "syslog" ] ; then
	    cat "$TCAT" >> "$TFILE"
	else
	    logger -p user.notice -t cron-apt -f "$TCAT"
	fi
    fi
}

# Create file with RUN information. Line data should be added later.
# Arguments:
# 1: File to create (if = "syslog" we should log to syslog instead)
# 2: File to cat e.g. $TEMP
# 3: Optional file to cat (like errormsg.d/action or so)
# 4: Optional runinformation file (will be removed)
# 5: Optional actioninformation file (will be removed)
# 6: If not empty data will be printed to stdout too.
createdivinfo() {
    FILE="$1"
    CAT="$2"
    OPTCAT="$3"
    OPTRUNCAT="$4"
    OPTACTIONCAT="$5"
    TMPSTDOUT="$6"
    if [ "$FILE" != "syslog" ] ; then
	touch "$FILE"
    fi
    if [ -n "$OPTCAT" ] && [ -f "$OPTCAT" ] ; then
	tomsg "$OPTCAT" "$FILE" "$TMPSTDOUT"
    fi
    if [ -n "$OPTRUNCAT" ] && [ -f "$OPTRUNCAT" ] ; then
	tomsg "$OPTRUNCAT" "$FILE" "$TMPSTDOUT"
	rm -f "$OPTRUNCAT"
    fi
    if [ -n "$OPTACTIONCAT" ] && [ -f "$OPTACTIONCAT" ] ; then
	tomsg "$OPTACTIONCAT" "$FILE" "$TMPSTDOUT"
	rm -f "$OPTACTIONCAT"
    fi
    tomsg "$CAT" "$FILE" "$TMPSTDOUT"
}

# ACTIONF as first argument
createmailinfo() {
    touch "$MAIL"
    if [ -n "$MAILON" ] && [ -f "$MAILMSGSDIR/$MAILON" ] ; then
	cat "$MAILMSGSDIR/$MAILON" >> "$MAIL"
    fi
    createdivinfo "$MAIL" "$TEMP" "$MAILMSGDIR/$1" "$RUNMAIL" "$ACTIONMAIL"
}

# ACTIONF as first argument
createsysloginfo() {
    if [ -n "$SYSLOGON" ] && [ -f "$SYSLOGMSGSDIR/$SYSLOGON" ] ; then
	logger -p user.notice -t cron-apt -f "$SYSLOGMSGSDIR/$SYSLOGON" 
    fi
    createdivinfo "syslog" "$TEMP" "$SYSLOGMSGDIR/$1" "$RUNSYSLOG" "$ACTIONSYSLOG"
}

# ACTIONF as first argument
createloginfo() {
    createdivinfo "$LOG" "$TEMP" "$LOGMSGDIR/$1" "$RUNLOG" "$ACTIONLOG" "$STDOUT"
}

# ACTIONF as first argument
createerrorinfo() {
    createdivinfo "$ERROR" "$TEMP" "$ERRORMSGDIR/$1" "$RUNERROR" "$ACTIONERROR"
}

# run-parts emulation, stolen from Branden's /etc/X11/Xsession
run_parts () {
	# reset LC_COLLATE
	unset LANG LC_COLLATE LC_ALL

	if [ -z "$1" ]; then
		echo >&2 "$0: internal run_parts called without an argument"
	fi
	if [ ! -d "$1" ]; then
		echo >&2 "$0: internal run_parts called, but $1 does not exist or is not a directory."
	fi
	for F in $(ls $1); do
		if expr "$F" : '[[:alnum:]_-]\+$' > /dev/null 2>&1; then
			if [ -f "$1/$F" ] ; then
				echo "$1/$F"
			fi
		fi
	done;
}

############################### check #########################################

if ! [ -d "$LIBDIR/$CONFIGDIRNAME" ]; then
    mkdir -p "$LIBDIR/$CONFIGDIRNAME"
fi
if ! [ -d "$MAILCHDIR" ]; then
    mkdir -p "$MAILCHDIR"
fi

################## terminate if $REFRAINFILE exists ##################

if [ -e "$REFRAINFILE" ]; then
    exit 0
fi

############################### sleep #########################################

echo "CRON-APT RUN [$CONFIG]: $(date)" > $INITLOG
if [ -n "$RUNSLEEP" ] ; then
    if [ $RUNSLEEP -gt 0 ] ; then
	if [ -z "$RANDOM" ] ; then
            # A fix for shells that do not have this bash feature.
	    RANDOM=$(dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -c"1-5")
	fi
	TIME=$(($RANDOM % $RUNSLEEP))
	sleep $TIME
	echo "CRON-APT SLEEP: $TIME, $(date)" >> $INITLOG
    fi
fi

cp "$INITLOG" "$RUNMAIL"
cp "$INITLOG" "$RUNLOG"
cp "$INITLOG" "$RUNSYSLOG"
cp "$INITLOG" "$RUNERROR"
rm -f "$INITLOG"

############################### script ########################################
# Always run onexit before exit.

for ACTION in $(run_parts "$ACTIONDIR") ; do
	ACTIONF=$(echo $ACTION | sed "s|$ACTIONDIR/||")
	if [ -f "$ACTIONCONFDIR/$ACTIONF" ] ; then
	    . "$ACTIONCONFDIR/$ACTIONF"
	fi
	echo "CRON-APT ACTION: $ACTIONF" > "$ACTIONERROR"
	echo "CRON-APT ACTION: $ACTIONF" > "$ACTIONMAIL"
	echo "CRON-APT ACTION: $ACTIONF" > "$ACTIONLOG"
	echo "CRON-APT ACTION: $ACTIONF" > "$ACTIONSYSLOG"
	cat "$ACTIONDIR/$ACTIONF" | \
	    sed -e "s/#.*$//;" | \
	    grep -v "^[[:space:]]*$" | {
		while read LINE ; do
		    echo "CRON-APT LINE: $LINE" > "$TEMP"
		    if [ -x /usr/bin/dotlockfile ] ; then
			if ! dotlockfile -l -p -r 10 $LOCKFILE; then
			    echo > $TEMP "cannot acquire apt lock."
			    RET=1
			else
			    UMASK_SAVE=$(umask)
			    umask $UMASK_RELAXED
			    $APTCOMMAND $OPTIONS $LINE >> $TEMP 2>&1
			    RET=$?
			    umask $UMASK_SAVE
			fi
			dotlockfile -u $LOCKFILE
		    else
		    	if [ -z "$NOLOCKWARN" ]; then
			    echo >  $TEMP "WARNING: dotlockfile not installed. If you don't want to see this"
			    echo >> $TEMP "         Warning any more, set NOLOCKWARN in the configuration file."
			fi
			UMASK_SAVE=$(umask)
			umask $UMASK_RELAXED
			$APTCOMMAND $OPTIONS $LINE >> $TEMP 2>&1
			RET=$?
			umask $UMASK_SAVE
		    fi
		    if [ "$FILTERCTRLM" = "true" ]; then
			grep -v "
" "$TEMP" > "$FILTER"
			cp "$FILTER" "$TEMP"
		    fi
		    if [ $RET -ne 0 ]; then
		        # ---
			# An error has occured.
			createerrorinfo $ACTIONF
			
			# SYSLOG
			if [ "$SYSLOGON" = "error" ] || [ "$SYSLOGON" = "upgrade" ] ; then
			    createsysloginfo $ACTIONF
			fi

			# MAIL
			if [ "$MAILON" = "error" ] || [ "$MAILON" = "upgrade" ] ; then
			    createmailinfo $ACTIONF
			fi
			
			# DEBUG
			if [ "$DEBUG" = "error" ] ; then
			    createloginfo $ACTIONF
			fi

			# EXIT
			if [ "$EXITON" = "error" ] ; then
			    echo exit > "$STATUS"
			    #exit
			fi
		    else
		        # ---
			# No error has occured.
			
			if grep -q 'The following packages will be upgraded' "$TEMP" ; then
		            # ---
			    # Upgrade has happend

	                    # SYSLOG
			    if [ "$SYSLOGON" = "upgrade" ] ; then
				createsysloginfo $ACTIONF
			    fi
			    # MAIL
			    if [ "$MAILON" = "upgrade" ] ; then
				createmailinfo $ACTIONF
			    fi
			    # DEBUG
			    if [ "$DEBUG" = "upgrade" ] ; then
				createloginfo $ACTIONF
			    fi
			fi
		    fi
		    # ---
                    # Independent of error or not
			
		    # SYSLOG
		    if [ "$SYSLOGON" = "always" ] ; then
			createsysloginfo $ACTIONF
		    fi
		    # MAIL
		    if [ "$MAILON" = "always" ] ; then
			createmailinfo $ACTIONF
		    fi
		    # DEBUG
                    if [ "$DEBUG" = "verbose" ] || [ "$DEBUG" = "always" ] ; then
			createloginfo $ACTIONF
		    fi

		    if grep -qvE '^[[:space:]]*$|CRON-APT' "$TEMP"; then
		        # ---
			# Now we have output
			if [ "$SYSLOGON" = "output" ]; then
			    createsysloginfo $ACTIONF
			fi
			if [ "$MAILON" = "output" ]; then
			    createmailinfo $ACTIONF
			fi
			if [ "$DEBUG" = "upgrade" ] ; then
			    createloginfo $ACTIONF
			fi
		    fi
		    TLINE=$(echo "$LINE" | sed -e "s/[[:space:]]/_/g;" | \
			tr "/" "-")
		    if [ ! -r "$MAILCHDIR/$ACTIONF-$TLINE" ] || ! diff -u "$MAILCHDIR/$ACTIONF-$TLINE" "$TEMP" > "$DIFF" ; then
			cp "$TEMP" "$MAILCHDIR/$ACTIONF-$TLINE"
			# What to do with diff
			# OBS! FROM NOW ON "$TEMP" CAN HAVE DIFF INFORMATION IN IT!
			if [ -n "$DIFF" ] && [ -r "$DIFF" ] ; then
			    if [ "$DIFFONCHANGES" = "only" ] ; then
				cp "$DIFF" "$TEMP"
			    elif [ "$DIFFONCHANGES" = "append" ] ; then
				cat "$DIFF" >> "$TEMP"
			    elif [ "$DIFFONCHANGES" = "prepend" ] ; then
				echo "----- DIFF END HERE -----" >> "$DIFF"
				cat "$TEMP" >> "$DIFF"
				mv "$DIFF" "$TEMP"
			    fi
			    rm -f "$DIFF"
			fi
		        # ---
			# We have changes

			# MAIL
			if [ "$MAILON" = "changes" ] ; then
			    createmailinfo $ACTIONF
			fi
			# SYSLOG
			if [ "$SYSLOGON" = "changes" ] ; then
			    createmailinfo $ACTIONF
			fi
			# DEBUG
			if [ "$DEBUG" = "changes" ] ; then
			    createloginfo $ACTIONF
			fi
		    fi
		done
		#exit
	    }
	if [ -e "$STATUS" ] ; then
	    if grep exit "$STATUS" > /dev/null 2>&1 ; then
		rm -f "$STATUS"
		onexit
		exit
	    fi
	    rm -f "$STATUS"
	fi
done

onexit
