#!/sbin/sh
#
# $Id: install_check,v 1.41 1998/11/04 15:53:56 casper Exp $
#
# Script to update a host form the install tree on the install
# server.  This script is designed to be run from cron at night.
#
# Note to people about this script: don't print any messages to
# stdout.  Stderr and stdout are used as record of modifications
# and failures.  Any message to stdout will cause a reboot.
# Any message to stderr, will prevent it and will cause
# email to root.
#
#  This script can be run from crontab with the following options
#
#    -r <seconds>	sleep a random time less than n seconds
#			(so all clients can be identical but won't
#			run all jobs at the same time)
#    -f			force execution (when users are present)
#    -n			don't update
#    -l			list all pathnames checked on stdout.
#    -k 		don't reboot or kill
#    -s			save diff output to $tmp/update.diff
#    -c			extra careful, don't run when users present,
#			don't even create a delayed file.
#    -C	<installdir>	Use the specified directory as "install" directory,
#			instead of getting it through bootparams
#    -R <root>		Act on the chrooted environment <root>
#
# Options to mask deficiency in cron.
#
#    -d <weekday>	run on <weekday> only, default is Mon.
#    -w			weekly (execute every <weekday> morning)
#    -m			monthly (first <weekday> morning of every month)
#
# Casper Dik (casper@fwi.uva.nl)

#
# set a nice default umask
#
umask 022

basedir="`dirname $0`"
basedir="`cd $basedir;pwd`"
#
# The directory basedir should contain the following programs:
# rsleep, bootname, checkattr, auto_install
#
PATH=/usr/sbin:/sbin:/etc:/bin:"$basedir"
export PATH

userprocs ()
# Processes that will be killed before a reboot, also used to test
# the presence of users on the system.
# In a chrooted environment, not processes will be found.
{
    ls -ln $root/proc | awk '$3 > 100 { print $9 }'
}

#
# Parse arguments
#
weekday=Mon
time=0
root=
export root

[ -t 0 ] && dryrun=true

run=true
while getopts vcsklfmnwr:d:C:R: c
do
    case $c in
    v) verbose=true;;
    f) dryrun=; force=true;;
    m) run=; month=true;;
    n) dryrun=true;;
    w) run=; week=true;;
    r) time=$OPTARG;;
    d) weekday=$OPTARG
	case "$weekday" in
	Mon | Tue | Wed | Thu | Fri | Sat | Sun ) ;;
	*) echo "${0}: $weekday isn't a valid day" 1>&2 ; exit 1;;
	esac
    ;;
    k) noreboot=true;;
    l) list=true;;
    s) diff=true;;
    c) care=true;;
    C) SI_CONFIG_DIR=$OPTARG;;
    R) root=$OPTARG; noreboot=true;;
    \?)  echo "Usage: $0 [-d day] [-vfslkn] [-m|w] [-r secs] [-C config] [-R root]" 1>&2; exit 2 ;;
    esac
done

shift `expr $OPTIND - 1`

rootopt=${root:+-R\ $root}

#
# All temporary files go in /var/sadm.
#
tmp=$root/var/sadm

if [ -n "$diff" ]
then
     exec 5>$tmp/update.diff 
fi

if [ $# != 0 ]
then
    echo "Usage: $0 [-d day] [-csklfn] [-m|w] [-r secs]" 1>&2
    exit 1
fi

[ -n "$month" ] && [ "`date +%d`" -le 7 ] && run=true
[ -n "$week" ] && [ "`date +%a`" = "$weekday" ] && run=true

tstamp=$root/var/sadm/last_update
dstamp=$root/var/sadm/update_delayed
rstamp=$root/var/sadm/must_reboot
[ -f $dstamp -a -z "$care" ] && force=true
[ -n "$run" -a -z "$dryrun$force" ] && [ -n "`userprocs`" ] && dryrun=yes
# Nothing to do.
[ -z "$run" -a -z "$dryrun" ] && exit 0

dontfix ()
{
    [ -n "$dryrun" ]
}

fatal ()
{
    if [ -s $tmp/update.stderr ]
    then
	cat $tmp/update.stderr 1>&4
    fi
    error "$@" 2>&4
    exit 1
}

error ()
{
    echo "*******" "$@" 1>&2
}

msg ()
{
    echo "$@" 1>&3
}

#
# Compute which pathname prefixes are not local to the system.
# This code breaks this script for diskless clients.
# It is only used for mkdir and copy since we create mountpoints
# (which when test are imported from the server) and some hidden
# files (needed when /opt cannot be mounted).
#
applic=
for path in `awk '$3 != "ufs" { print $2 }' < $root/etc/mnttab`
do
    path=$root$path
    if [ -z "$applic" ]
    then
	applic="$path|$path/*"
    else
	applic="$applic|$path/*|$path"
    fi
done
if [ -n "$applic" ]
then
    applic='case $1 in '"$applic"') return 0;; *) return 1;; esac'
else
    applic='return 1'
fi

notapplicable ()
{
    eval "$applic"
}

#
# Do locking
#
undo="rm -f $tmp/LOCK.$$"

rmnologin=

trap 'eval $undo; eval $rmnologin' 0
trap 'exit 1' 1 2 3 4 5 6 7 15

touch $tmp/LOCK.$$
ln -n $tmp/LOCK.$$ $tmp/update_lock || {
	echo "*** install_check locked" 1>&2
	exit 1
}
rm -f  $tmp/LOCK.$$
undo="rm -f $tmp/update_lock $tmp/update.stdout $tmp/update.stderr $tmp/patchlog"

#
# Redirect stdout, stderr. Old stdout is preserved on 3, old stderr on 4.
#

exec 3>&1 4>&2 > $tmp/update.stdout 2> $tmp/update.stderr
if [ -n "$verbose" ]
then
    pids=`
	tail -f $tmp/update.stdout 1>&3 2>/dev/null &
	pids=$!
	tail -f $tmp/update.stderr 1>&4 2>/dev/null &
	pids="$pids $!"
	touch $tmp/patchlog
	tail -f $tmp/patchlog 1>&3 2>/dev/null &
	echo $pids $!`
    undo="$undo; kill $pids"
fi

#
# First, we need the real hostname.  Bootname is a program that
# first retrieves uname, followed by a gethostbyname, followed by
# a gethostbyaddr, which is what sort-of happens during net booting.
#
short_name="`cat $root/etc/nodename`"
long_name=`bootname $short_name`

if [ -z "$long_name" ]
then
    fatal "Can't find hostname of $short_name"
fi



dontfix || {
    do=' was'
    rsleep $time
    if [ "$noreboot" != true ]
    then
	rmnologin="rm -f /etc/nologin"
	cat > /etc/nologin << EOF
NO LOGINS!

System update in progress.

EOF
    fi
}

if [ -z "$SI_CONFIG_DIR" ]
then
    bparms=`/sbin/bpgetfile install_config "" $long_name`

    # $bparms is "  " when there's no answer.
    set -- $bparms
    if [ $# != 3 ]
    then
	fatal "Can't find bootparam install_config for $long_name"
    fi
    install_server=$1
    install_addr=$2
    install_config=$3

    if [ "$install_server" != $long_name -a "$install_server" != $short_name ]
    then
	if mount -r ${install_server}:$install_config /mnt
	then
	    undo="cd /;umount /mnt ; $undo"
	    SI_CONFIG_DIR=/mnt
	else
	    fatal "Can't mount ${install_server}:$install_config on /mnt"
	fi
    else
	SI_CONFIG_DIR=$install_config
    fi
fi

export SI_CONFIG_DIR
iroot=$SI_CONFIG_DIR

if [ ! -x $iroot/bin/version ]
then
    fatal "install tree version unknown."
fi

#
# XXX: -R option doesn't work quite right here.
#
instrel="`$iroot/bin/version`"
if [ -z "$root" ]
then
    currel="`uname -r`"
else
    if [ -f $root/var/sadm/softinfo/INST_RELEASE ]
    then
	. $root/var/sadm/softinfo/INST_RELEASE
    elif [ -f $root/var/sadm/system/admin/INST_RELEASE ]
    then
	. $root/var/sadm/system/admin/INST_RELEASE
    else
	fatal "can't determine OS revision of $root"
    fi
    currel=`echo $VERSION | sed -e 's/^2\.//' -e 's/^/5./'`
fi

if [ "$instrel" != "$currel" ]
then
    if [ -n "$instrel" -a -n "$currel" ]
    then
	# Check for a real upgrade, not a downgrade
	set -- `IFS=.; echo $currel`
	for i in `IFS=.; echo $instrel`
	do
	    if [ $i -lt $1 ]
	    then
		fatal "install tree contains older OS"
	    fi
	    if [ $i -gt $1 ]
	    then
		# auto_install
		fatal "want to auto_install $instrel"
	    fi
	    shift; if [ $# -eq 0 ]; then break; fi
	done
	if [ $# -eq 0 ]
	then
	    # auto_install
	    fatal "want to auto_install $instrel"
	else
	    fatal "install tree contains older OS"
	fi
    fi
    fatal "install tree version mismatch, ${instrel:-unknown} != $currel"
fi

#
#  location of files to install
#
install=${SI_CONFIG_DIR}/install
scripts=scripts
files=files

domainname="`domainname`"
hostname=$long_name
#
# This code is copied from scripts/finish.  It should be indentical.
#
#  findfiles locates a file in the directory ${SI_CONFIG_DIR}/install
#  and returns all matches (the hostname, one match for each class
#  (either class.domainname or class) the domainname.
#  If there are no matches, it returns default.
#
#  findfile returns only the first match
#
# Search order is:
#	${install}/<file name>   if it's a file.
#	${install}/<file name>/<system name>
# Then for each class to which a machine belongs:
#	${install}/<file name>/<class>.<yp domain name>
#	${install}/<file name>/<class>
# And finally:
#	${install}/<file name>/<yp domain name>
#	${install}/<file name>/default  (only returned when no other matches)
#

findfile ()
{
    findfiles -1 "$1"
}

findfiles ()
{
    match=
    if [ x"$1" = x-1 ]
    then
	shift;
	one=true
    else
	one=
    fi
    if [ -f ${install}/$1 ]
    then
	echo ${install}/$1
	return
    elif [ -f ${install}/$1/$hostname ]
    then
	echo ${install}/$1/$hostname
	[ -n "$one" ] && return
	match=true
    fi
    for class in $classes
    do
	if [ -f ${install}/$1/"$class.$domainname" ]
	then
	    echo ${install}/$1/"$class.$domainname"
	    [ -n "$one" ] && return
	    match=true
	elif [ -f ${install}/$1/$class ]
	then
	    echo ${install}/$1/$class
	    [ -n "$one" ] && return
	    match=true
	fi
    done
    if [ -f ${install}/$1/"$domainname" ]
    then
	echo ${install}/$1/"$domainname"
    elif [ -z "$match" ]
    then
	echo ${install}/$1/default
    fi
}

findconf ()
{
    if [ -f ${install}/$1/common ]
    then
	echo ${install}/$1/common
    fi
    set -- `findfiles $1`
    # We must return at least one file.
    if [ ! -r "$1" ]
    then
	echo /dev/null
    else
	echo $@
    fi
}

# Strip comments and blank lines from conf files.
readconf ()
{
    conf=`findconf $1`
    [ -n "$confverbose" ] && echo "$1	=" $conf 1>&2
    egrep -h -v '^#|^[	 ]*$' $conf
}

#
# We'll only check newly installed files, links, symlinks and directories.
#

cd $install

classfile=`findfile class`
if [ ! -f "$classfile" ]
then
    classfile=/dev/null
fi
classes=`cat "$classfile"`
export classes
for f in ${install}/class/S[0-9]*.sh
do
    if [ -x $f ]
    then
	classes="$classes `. $f`"
    fi
done
>$root/etc/INSTALL_CLASS
chmod 644 $root/etc/INSTALL_CLASS
for class in $classes
do
    echo $class >> $root/etc/INSTALL_CLASS
done
#
#	Create directories in the mkdir.conf file
#
readconf mkdir.conf | while read owner group mode dir
do
    dir=$root$dir
    if notapplicable $dir; then continue; fi
    [ -z "$list" ] || msg $dir
    if [ ! -d $dir ]
    then
	echo $dir$do missing
	# To fix:
	dontfix || {
	    mkdir -p $dir &&
	    chown $owner $dir &&
	    chgrp $group $dir &&
	    chmod $mode $dir
	} || error "Can't mkdir $dir"
    elif checkattr $dir $owner $group $mode
    then
	:
    else
	chown $owner $dir &&
	chgrp $group $dir &&
	chmod $mode $dir
    fi
done

#
#     touch files in the touch.conf file
#
readconf touch.conf | while read target owner group mode comment
do
    target=$root$target
    if [ ! -f $target ]
    then
	echo $target$do missing
	dontfix || {
	    touch $target &&
	    chown $owner $target &&
	    chgrp $group $target &&
	    chmod $mode $target
	} || error "Can't create $target"
    elif checkattr $target $owner $group $mode
    then
	:
    else
	chown $owner $target &&
	chgrp $group $target &&
	chmod $mode $target
    fi
done

#
#	Copy files in the copy.conf file
#
linkcount ()
{
    set -- `ls -dn $1 2> /dev/null`
    echo ${2:-0}
}
readconf copy.conf | while read source destdir owner group mode comment
do
    file=`findfile ${files}/$source`
    [ "$destdir" = / ] && destdir=
    destdir=$root$destdir
    dest="$destdir/$source"
    if notapplicable $dest; then continue; fi
    [ -z "$list" ] || msg $dest
    if [ -d "${destdir:-/}" -a -r "$file" ]
    then
	if cmp -s "$file" "$dest"
	then
	    if checkattr "$dest" $owner $group $mode
	    then
		:
	    else
		chmod $mode $dest &&
		chown $owner $dest &&
		chgrp $group $dest
	    fi
	else
	    echo $dest"${comment:+ ($comment)}"$do missing or changed
	    if [ -f $dest -a -n "$diff" ]
	    then
		echo "diff -c $file $dest" 1>&5
		diff -c $file $dest 1>&5 2>&5
	    fi
	    dontfix || {
		# Only remove a file if the link cont is 1,
		# we do this because we're lazy enough not to
		# have link.conf 100% correct.
		nlinks=`linkcount $dest`
		if [ -f $dest -a ! -f $dest.FCS ]
		then
		    if [ "$nlinks" = 1 ]
		    then
			mv $dest $dest.FCS
			nlinks=0
		    else
			cp -p $dest $dest.FCS
		    fi
		    chmod ug-s,a-w,og-x,-t $dest.FCS
		fi
		if [ "$nlinks" = 1 ]
		then
		    rm -f $dest
		fi
		cp -p $file $dest &&
		chmod $mode $dest &&
		chown $owner $dest &&
		chgrp $group $dest
	    } || error "Can't install $dest"
	fi
    else
	error "destination ${destdir:-/}, or source $file doesn't exist"
    fi
done

#
#	create symbolic links in the slink.conf file
#
readlink ()
{
    (ls -l $1 | awk '{ print $11 }') 2>/dev/null
}

readconf slink.conf | while read filename linkname
do
    linkname=$root$linkname
    if notapplicable $linkname; then continue; fi
    [ -z "$list" ] || msg $linkname
    if [ ! -h $linkname -o "`readlink $linkname`" != $filename ]
    then
	echo symlink $linkname to $filename$do missing or changed
	# To fix:
	dontfix || {
	    rm -f $linkname &&
	    ln -s $filename $linkname
	} || error "Can't link $filename $linkname"
    fi
done

#
#	Create hard links in the link.conf file
#
niequal ()
{
    set -- `ls -i $1 $2 2>/dev/null`
    [ "$1" != "$3" -o -z "$1" ]
}

readconf link.conf | while read filename linkname
do
    linkname=$root$linkname
    filename=$root$filename
    if notapplicable $linkname; then continue; fi
    [ -z "$list" ] || msg $linkname
    if niequal "$filename" "$linkname"
    then
	echo hardlink $linkname to $filename$do missing or changed
	# To fix:
	dontfix || {
	    rm -f $linkname &&
	    ln $filename $linkname
	} || error "Can't link $filename $linkname"
    fi
done

#
# Update scripts, when simple file changes aren't enough.
# XXX: Update scripts should know about $root
#
updatedir=$SI_CONFIG_DIR/updates
if [ -d $updatedir ]
then
    if [ ! -f $tstamp ]
    then
	updaters=`ls -tr $updatedir/*`
    else
	updaters=`find $updatedir -newer $tstamp -type f -print`
	if [ -n "$updaters" ]
	then
	    updaters=`ls -tr $updaters`
	fi
    fi
    for i in $updaters
    do
	if dontfix
	then
	    echo Must update with $i
	else
	    . $i
	    cp -p $i $tstamp
	fi
    done
fi

#
# And now for the patches.
#
#

do-patch $rootopt -n ${dryrun:+-d} > $tmp/patchlog 2>&1


# Succeeded patches to stdout (include patches that call backout patch)
grep -v 'failed.$' $tmp/patchlog
# Failed patches to stderr
grep 'failed.$' $tmp/patchlog 1>&2

# At this point we redirect stdin and stderr to what they used to be
# and close the $tmp/update.* files.

exec 1>&3 2>&4 3>&- 4>&- 5>&-

if dontfix
then
    if [ -n "$run" -a "$dryrun" = yes ]
    then
	if [ -s $tmp/update.stdout ]
	then
	    # Force update next time.
	    touch $dstamp
	fi
    else
	# Show output (on stderr, 'cuz we want to list on stdout)
	cat $tmp/update.stdout $tmp/update.stderr 1>&2
    fi
else
    if [ -s $tmp/update.stdout ]
    then
	# Make a stamp that we really must reboot next time.
	if [ -n "$noreboot" ]
	then
	    touch $rstamp $dstamp
	fi
	(date +"stdout of %C";
	cat $tmp/update.stdout; echo ----
	) >> $root/var/sadm/update.log
    elif [ ! -f $rstamp ]
    then
	# From previous install_check -k.
	noreboot=true
    fi
    if [ -s $tmp/update.stderr ]
    then
	if [ -z "$verbose" ]
	then
	    echo The following errors occured during update of ${long_name}:
	    cat $tmp/update.stderr
	fi
	(date +"stderr of %C";
	cat $tmp/update.stderr; echo ----
	) >> $root/var/sadm/update.log
	noreboot=true
    fi
fi

[ -n "$noreboot" ] || dontfix || {
	if [ -n "`userprocs`" ]
	then
		# Indent tabs, not spaces.
		wall <<- EOF > /dev/null 2>&1
		The system will be rebooted shortly by auto-update [$$].
		EOF
		sleep 120
	fi
	# Indent tabs, not spaces.
	wall <<- EOF > /dev/null 2>&1
	The system will be rebooted now by auto-update.
	EOF
	sigs='TERM HUP KILL'
	for sig in $sigs
	do
	    # Kill non system processes.
	    procs=`userprocs`
	    if [ -z "$procs" ]
	    then
		break;
	    fi
	    for proc in $procs
	    do
		kill -$sig $proc > /dev/null 2>&1
	    done
	    sleep 15
	done
	rm -f $dstamp
	telinit 6
	rmnologin=
}
