#!/bin/sh # # Freshmeat release: 1.2 # # =head1 NAME # # runmaint - run a maintenance script # # =head1 SYNOPSIS # # runmaint [B<-s> I] [B<-date> I] [B<-x>] I [I] # # =head1 DESCRIPTION # # B is a wrapper for cron(8) jobs which # lets the user keep their crontab static and simple, # moves all the shell complexity into scripts, # supplies a standard, configurable, environment to the scripts, # cascades regular scripts through a simple naming scheme # and delivers the output of scripts as email with a useful subject line. # Optionally, it will deliver to a log file # or standard output and standard error. # # See: # # http://www.cskk.ezoshosting.com/cs/runmaint.html # # for more detailed info. # # =head1 INSTALLATION # # Normally I is installed as part of the B package in , # and the examples below are written on that basis. # # =head1 PERSONAL SETUP # # Create a directory B<.maint> in your home directory. # Scripts run from cron(8) go here. # # Create a crontab file with the crontab(1) command. # Typically this is a single line such as: # # 0 * * * * exec /opt/css/bin/runmaint hourly # # or # # 0 0 * * * exec /opt/css/bin/runmaint daily # # The word after the B command is the name of a script to run # from the B<~/.maint> directory. # # The words B, B, B, B and B # are special. # If a script with this name is run and happens to fall on the # due time for the less frequent script # then that script is also run on completion of the more frequent script. # In this way the hourly cron line above also causes the B, B, # B and B scripts to be run as appropriate. # Further, unlike multiple cron lines of different frequency, # the timing is guarrenteed not to overlap nor is there undue delay # between the termination of the more frequent script # and commencement of the subsequent less frequent script. # # Also, whenever any script B is requested # B also looks for scripts named B> # and B@I> # and runs them if they are present. # I and I take their names from the environment variables # B<$HOST> and B<$SYSTEMID> respectively. # I normally arrange these to be the short hostname (i.e. without the domain) # and an identifier-like name for the local administrative domain # (eg B for my home machines). # With this mechanism # you can place the cron line on many machines # and still perform specific actions on particular machines # simply by adding suitably named scripts. # If B<$HOST> or B<$SYSTEMID> are missing, # B infers B<$HOST> from the first component of the output of the hostname(1) command, # and sets B<$SYSTEMID> to B. # # Additionally, # if B names a directory # then all files in the top level of that directory are run. # # =head1 ENVIRONMENT # # B prepopulates the environment # with some convenience variables # reflecting the date and time # that B commenced. # These are lowercase strings derived from the output of date(1), and are: # # wday The day of the week, eg "tue". # mon The month, eg "apr". # mday The month day, eg 23. # hh The hour, eg 09. # mm The minute, eg 13. # ss The seconds, eg 00. # tz The timezone. # year The year, eg 1998. # # These are passed to any cascaded scripts intact # i.e. if a 6pm runmaint job kicks off and runs long enough # that some cascased jobs commences much later # then the convenience variables will still be for 6pm. # # Before running any script # B sources the file # B. # Systemwide default environment settings go in here as shell commands. # After this the file # B<~/.maint/env> is sourced. # Personal environment settings go in here as shell commands. # # Usually you will want the cron environment to resemble the login # environment closely # and so B would normally contain the line # # . /etc/profile # # and the B<~/.maint/env> file would reasonably contain the line # # . $HOME/.profile # # in this way sourcing the user's normal startup files. # # =head2 RUNMAINT_DIR # # The installation prefix, typically C or C. # The default comes from B<$OPTCSS> (default: B) # fitting the syncopt scheme as described here: # # http://www.cskk.ezoshosting.com/cs/syncopt/ # # The B package itself is here: # # http://www.cskk.ezoshosting.com/cs/css/ # : ${OPTCSS:=/opt/css}; export OPTCSS : ${RUNMAINT_DIR:=$OPTCSS} MaintDir=$RUNMAINT_DIR # tack on a few things to help find stuff like whoami PATH=$PATH:/usr/ucb export PATH [ -s "$OPTCSS/env.sh" ] && . "$OPTCSS/env.sh" : ${SYSTEMID:=localdomain} : ${USER:=`whoami || id -un`} : ${HOST:=`hostname`} case $HOST in *.*) HOST=`expr "x$HOST" : 'x\([^.]*\)\..*'` ;; esac export USER HOST SYSTEMID USER=${USER:-$LOGNAME}; export USER [ -n "$USER" ] || { echo "$0: no \$USER or \$LOGNAME, bailing out" >&2 exit 1 } # debugging check case "$USER" in [0-9]*) echo "WARNING @ `hostname`: USER=\"$USER\", LOGNAME=\"$LOGNAME\", id=`id`" >&2 ;; esac if [ -f "$0" ] then RUNMAINT="$0 -E" else RUNMAINT="$RUNMAINT_DIR/bin/runmaint -E" fi RunMaint=$RUNMAINT RUNMAINT_ETC=$RUNMAINT_DIR/etc/runmaint MaintEtc=$RUNMAINT_ETC : ${RUNMAINT_SCRIPTS:=$HOME/.maint} MaintScripts=$RUNMAINT_SCRIPTS export RUNMAINT_DIR MaintDir \ RUNMAINT_SCRIPTS MaintScripts \ RUNMAINT RunMaint usage="Usage: $0 [-d dir] [-s subj] [-date datestr] [-x] target [addresses] See: http://www.cskk.ezoshosting.com/cs/runmaint.html for more detailed information. -d dir Directory containing maintenance scripts. -s subj Subject for mail report. -date datestr Use this instead of the date command output. -x Trace execution. target frequency[.host] or an ad hoc name frequency daily Soon after 12am. dow After daily, each day of the week \"dow\". monthly After daily/weekly, on first of month. yearly After monthly, on 1 January. host Stuff for a specific host." # save invocation for logging invocation="$0 $*" # special case noenv as first arg noenv= [ "x$1" = x-E ] && { noenv=1; shift; } badopts= args=$* trace= xflag= traceon=: traceoff=: subj="runmaint: $USER@$HOST" datestr= # # =head1 OPTIONS # # =over 4 # while : do case $1 in --) shift; break ;; # # =item B<-d> I # # Specify the directory containing maintenance scripts. # -d) RUNMAINT_SCRIPTS=$2; shift case "$RUNMAINT_SCRIPTS" in /*) ;; *) RUNMAINT_SCRIPTS=`pwd`/$RUNMAINT_SCRIPTS ;; esac MaintScripts=$RUNMAINT_SCRIPTS export RUNMAINT_SCRIPTS MaintScripts ;; # # =item B<-s> I # # Set the subject line for the notification email. # -s) subj=$2; shift ;; # # =item B<-date> I # # Use I instead of the date command output. # This is really a I internal option # for passing the ancestors runmaint start date forward # to subsequent scripts in the same run. # -date) datestr=$2; shift ;; # # =item B<-x> # # Trace execution. # -x) xflag=-x trace=set-x traceon='set -x' traceoff='set +x' ;; -?*) echo "$0: unrecognised option: $1" >&2 badopts=1 ;; *) break ;; esac shift done if [ $# = 0 ]; then echo "$0: missing target" >&2; badopts=1 else target=$1; shift fi # # =item I # # A set of email addresses to which to deliver the output, if any. # These should be the core addresses such as I or Idomain> # rather than full RFC822 addresses like "Bfred@nerk.comE>" # as they are used unquoted, and are thus subject to whitespace separation etc. # # If I is simple "B<->" # then the output is delivered to standard output and standard error # rather than being redirected to email, # sometimes useful for running from the command line or for special purposes. # # If no I are present # then the default is to email the user # as defined by the B<$EMAIL> environment variable # or failing that the B<$USER> environment variable. # However, # if the directory B<~/.maint/log> is present # then the absense of I causes output to be delivered # to the log file B<~/.maint/log/I@I>. # Further, if that is a directory then output is delivered to the log file # B<~/.maint/log/I@I/I> # where I is the result of the datecode(1) command. # addresses=$* # # =back # # note starting time if [ -n "$datestr" ] then set -- $datestr else set -- `date|tr '[A-Z]:' '[a-z] '` fi if [ $# = 8 ] then wday=$1 mon=$2 mday=$3 hh=$4 mm=$5 ss=$6 tz=$7 year=$8 datestr=$* export wday mon mday hh mm ss tz year datestr else echo "$0: bad date arguments: $*" >&2 badopts=1 fi [ $badopts ] && { echo "$usage" >&2; exit 2; } [ $noenv ] \ || { [ -s "$RUNMAINT_ETC/env" ] && . "$RUNMAINT_ETC/env"; } xit=0 case $target in /*) try=$target ;; *) try="$RUNMAINT_ETC/$USER/$target $RUNMAINT_SCRIPTS/$target" ;; esac # runscript pathname [addrs...] runpath() { _rp=$1; shift ##echo "runpath $_rp..." >&2 _rp_addrs=$* _rp_dir=`dirname "$_rp"` || return 1 _rp_env=$_rp_dir/env [ -s "$_rp_env" ] || _rp_env= _rp_xit=0 if [ -d "$_rp/." ] then ##echo "$_rp is a directory" >&2 for _rp_f in "$_rp/"* do [ -f "$_rp_f" ] || continue ##echo "dirscript $_rp_f ..." >&2 runscript "$_rp_f" $_rp_addrs || _rp_xit=1 done else if [ -f "$_rp" ] then ##echo "script $_rp ..." >&2 runscript "$_rp" $_rp_addrs || _rp_xit=1 fi fi return $_rp_xit } # runscript scriptpath [addrs...] runscript() { _rs=$1; shift _rs_addrs=$* _rs_dir=`dirname "$_rs"` || return 1 _rs_xit=0 # log every run here as single line echo "`date` `hostname` $_rs [$invocation]" >>"$_rs_dir/.runlog" if [ -z "$_rs_addrs" ] then # if no addresses # then write a log if there's a log dir # otherwise email the user _rs_logdir=$_rs_dir/log if [ -d "$_rs_logdir/." ] then _rs_log=$_rs_logdir/`basename "$_rs"` || return 1 [ -d "$_rs_log/." ] && { _rs_log=$_rs_log/`datecode` || return 1; } else _rs_addrs=${EMAIL:-$USER} fi fi _rs_merge=1 # merge stdout and stderr case "$_rs_addrs" in '') _rs_logdir2=`dirname "$log"` || return 1 mkdir -p "$_rs_logdir2" || return 1 ;; -) _rs_merge= ;; esac ( [ -n "$_rp_env" ] && . "$_rp_env" [ $_rs_merge ] && exec 2>&1 [ -x "$_rs" ] || { echo "$cmd: $_rs: not executable" >&2; exit 1; } exec sh $xflag "$_rs" ) \ | case "$_rs_addrs" in '') exec sh -c "exec >>'$_rs_log' echo echo '============================' echo 'command=$invocation' echo 'script=$_rs' date exec execif cat" ;; -) exec cat ;; *) _rs_subj=$subj [ -x "$_rs" ] || _rs_subj="NOT EXECUTABLE: $_rs_subj" [ -n "$_rs_subj" ] || _rs_subj=runmaint@$HOST _rs_subj="$_rs_subj $_rs $args" exec mailif -s "$_rs_subj" $_rs_addrs ;; esac } for script in $try do ##echo "$script ..." >&2 # run the script if present runpath "$script" $addresses || xit=1 osubj=$subj for ext in "$HOST" "$HOST@$SYSTEMID" do localscript=$script.$ext subj="$osubj - ($ext)" runpath "$localscript" $addresses || xit=1 done subj=$osubj done # run the implied scripts case $target in hourly) $trace $RUNMAINT -date "$datestr" $xflag hourly.$hh $addresses || xit=1 if [ $hh -eq 0 ] then $trace $RUNMAINT -date "$datestr" $xflag daily $addresses \ || xit=$? fi ;; daily) $trace $RUNMAINT -date "$datestr" $xflag "$wday" $addresses || xit=$? $trace $RUNMAINT -date "$datestr" $xflag "daily.$wday" $addresses || xit=$? if [ "x$wday" = xmon ] then $trace $RUNMAINT -date "$datestr" $xflag weekly $addresses \ || xit=$? fi if [ $mday -eq 1 ] then $trace $RUNMAINT -date "$datestr" $xflag monthly $addresses \ || xit=$? fi ;; monthly) if [ $mon = jan ] then $trace $RUNMAINT -date "$datestr" $xflag yearly $addresses \ || xit=$? fi ;; esac exit $xit # # =head1 SEE ALSO # # crontab(1), cron(8), datecode(1), mailif(1) # # =head1 AUTHOR # # Cameron Simpson Ecs@zip.com.auE 15oct1996 #