: #	@(#)ppurge	1.3	purge user files into purgatory
# ppurge:  "pseudo-purge" or "purgatory purge"
#	After a user account is deactivated, all the files owned by the
#	inactive account should be either removed or delivered to new
#	owners.  ppurge finds all files in the system owned by the
#	deactivated user and moves them (within the same filesystem so
#	space is not a consideration) to ROOT/purgatory, where ROOT is
#	the root directory of each filesystem.  ppurge is to be
#	optionally invoked by the rmuser script, but can be invoked by
#	hand.  It will barf and die harmlessly if invoked for a
#	superuser account or certain other system accounts listed in
#	BARFUSERS.  This script is SLOW and should be run in the
#	background.
# Author:  Larry Bamford, AO, OAT, STD, SEB, 8/8/90.
# Modifications:
# 10/11/91, Larry Bamford.  Fixed dire bug due to Unisys OS 6.03 bug in
#	find where -mount option being first caused zero file names to
#	be printed with a successful exit value.  This resulted in user
#	files being deleted without being moved to purgatory, the very
#	bug this project was commissioned to fix!

STATUSFILE=${ADM:-/adm}/lastrites export STATUSFILE
PATH=/bin:/etc:/usr/bin:/adm export PATH	# for security
BARFUSERS="daemon bin sys adm uucp nuucp sync lp listen mdqs"
ETC=${ETC:-/etc} export ETC
CMD=`basename $0`
SA=${LOGNAME:-admin}	# recipient of escapee report
STARTDIR=`pwd`		# for locating the nohup.out file, if necessary

USAGE="
Usage:  nohup $CMD [[-f fsroot] ...] Ousername 2>&1 | mail yourself &

where Ousername is a former user account name, username, prefixed with
the uppercase letter O.  $CMD will find all files in the system owned
by Ousername, and move them to a parallel hierarchy in the same
filesystem under ROOT/purgatory.  All relative positioning information
as well as owner, group and mode will be preserved.  No files are
removed.  Only minimal additional space is needed for new directories.
Superuser privilege is required.  If a superuser or certain other
system accounts are given as Ousername, $CMD will barf and die
harmlessly.  Currently the list of unacceptable other accounts includes 
$BARFUSERS.

The -f option allows specification of a single file system to consider,
instead of all mounted file systems.  Multiple -f options may be used.
Specify a file system by the full pathname of its root directory.  
"

# Process command line.
set -- `getopt f:dh\? $*`
if test $? != 0
then
	echo "$USAGE" 1>&2
	exit 2
fi
for i in $*
do case $i in
	-f)	ARG=$2
		FS_I=`ls -id $ARG | awk '{print $1}'`
		if test "$FS_I" != "2"
		then 
			cat >&2 <<-!
			$ARG is not a file system root.  Quit.
			!
			exit 1
		fi
		FSS="${FSS:+${FSS} }$ARG"	# add it to the list
		shift 2
		;;
	-d)	DEBUG=true; shift ;;
	-[h\?])	echo "$USAGE"; exit 0;;
	--)	shift; break;;
	esac
done
if test $# -ne 1
then
	echo "$CMD:  no account name given" 1>&2
	echo "$USAGE" 1>&2
	exit 1
fi
OUSER=$1

# Define status reporting function.
status() {
	echo "$OUSER|`date`|$1" >> $STATUSFILE
}

if test "$TESTING" != ""
then
	status testing $CMD
fi

# Reject BARFUSERS, superusers, active users, and non-existent users.
OUID=`grep "^$OUSER:" $ETC/passwd | awk -F: '{print $3}'`
if test "$OUID" = "0"
then
	echo "$CMD:  Cannot $CMD a superuser. ($OUSER)" 1>&2
	echo "$USAGE" 1>&2
	exit 3
fi
for user in $BARFUSERS
do
	if test "$OUSER" = "$user"
	then
		echo "$CMD: Cannot $CMD a system account. ($OUSER)" 1>&2
		echo "$USAGE" 1>&2
		exit 3
	fi
done
case "$OUSER" in
O*)
	if grep "^$OUSER:" $ETC/passwd > /dev/null
	then
		: ok
	else
		echo "$CMD: Cannot $CMD a non-existent user. ($OUSER)" \
		    1>&2
		echo "$USAGE" 1>&2
		exit 3
	fi
	;;
*)
	echo "$CMD: Cannot $CMD an active user. ($OUSER)" 1>&2
	echo "$USAGE" 1>&2
	exit 3
	;;
esac

# must be root to run this program
ID=`id`
TMP=`expr "$ID" : '.*\(uid=0(\)'`
if test -z "$TMP"
then
	echo "$CMD: you must be root or admin to use this program"
	exit 3
fi

# Okay, begin the real work.
status "begin $CMD"
trap "" 1 2 3	# resist stray interrupts
trap "status \"$CMD KILLED\";	# leave evidence for the coroner
exit 15" 15

# Use the find -mount option if it exists (SVR3 and later).
# BUG ALERT!  If -mount is the first option to find under Unisys OS
# 6.0, then no files will be found, and yet find will return
# successfully.  It doesn't matter much here, but in other places this 
# can cause VERY nasty things to happen!  10/11/91, DLB
> /tmp/foo$$file
if find /tmp/foo$$file -print -mount > /dev/null 2>&1
then MOUNTOPT="-mount"
else MOUNTOPT=
fi
rm /tmp/foo$$file 2>/dev/null

# Find the list of file system root directories from the list of mounted
# file systems.  This assumes both that all file systems are mounted,
# and that the mount table is healthy.  I suspect that if the mount
# table is corrupt the SA will find out quickly and probably not run
# this program.  If not, then some files may be missed and this script 
# will need to be run again.

ROOTS=`/etc/mount | fgrep -v '/remote ' | awk '{print $1}'`
export ROOTS

# Find which name is used for the /fjc (/app) file system so we can
# grep out duplicate reports of the linked alternate root name.
for fs in $ROOTS
do
	case "$fs" in
	/app)	FS_DUPL=/fjc; break;;
	/fjc)	FS_DUPL=/app; break;;
	esac
done
export FS_DUPL

# The following code is only necessary if find cannot be used with the
# new -mount option.  This will be slower and less elegant.

case "$MOUNTOPT" in
-mount) : skip this stuff ;;
*)	: else

# Compile list of all files and directories in THE root directory which
# are not mount point directories.  We are proceeding now on the
# assumption that all file systems are mounted onto directories in the
# topmost root directory.  This will not necessarily be the case for
# file systems such as /usr/spool, /usr/news, etc.

status "analyze root directory"
TMP1=/tmp/rf$$ export TMP1
TMP2=/tmp/rts$$ export TMP2
trap "rm -f $TMP1 $TMP2" 0
ls -a / | egrep -v "^\.$|^\.\.$" | sed 's/^/\//' | sort > $TMP1
for root in $ROOTS
do
	echo $root
done | sort > $TMP2
# grep out the duplicate filesystem mount point
ROOTFILES=`comm -23 $TMP1 $TMP2 | egrep -v "${FS_DUPL}"`
rm -f $TMP1 $TMP2
trap 0	# reset trap
;; # case -mount option doesn't exist

esac

# OK, we now have a list of all file system directories, as well as the
# list of files and directories in the root directory residing on the 
# root file system, if necessary.  Run the big loop(s).

# Notes on error handling.  It would be nice to capture all errors and
# report them on a per-filesystem basis.  But the commands in the
# pipelines do not necessarily end at the same time, nor does the
# conjunction (||) wait for all commands in the preceding pipeline to
# complete before proceeding.  Thus, the reports for each file system
# may be incomplete, or worse yet, contain messages for earlier
# filesystems in the loop.  If anybody finds a solution to this dilemma,
# please let me know.  Until then a single large error report is
# provided here.

# Initialize the "error" report.  (There will always be normal cpio
# output.)
ERRLOG=/tmp/pperrs$$ export ERRLOG
exec 2>$ERRLOG	# capture errors of the shell itself
ENDMSG="$CMD of $OUSER COMPLETED
Remember to check $STARTDIR/nohup.out for errors, 
if it exists.  The following output was captured on stderr.
\"0 blocks\" messages are normal.  Anything else should be examined
carefully before running pflush.  An attempt has been made to label the
output by filesystem.
"
echo "$ENDMSG" 1>&2
export FILELIST		# so can use value in subshells
case "$MOUNTOPT" in
-mount)
	# This case is actually more accurate, since it doesn't depend
	# on all filesystems being mounted at the top level.
	# Use file systems specified on command line, if any.  Else use
	# all mounted file systems.  
	for fs in ${FSS:-${ROOTS}}
	do
		# Create list of Ouser's files, per fs, as a byproduct.
		FILELIST=/tmp/Ofiles.`/bin/basename $fs` export FILELIST
		PROCEED=true	# proceed w/the removals by default
		# Skip purgatories and and the filesystem's root 
		# directory itself.  No need to skip $FS_DUPL because
		# of the -mount option.  Pipeline error messages 
		# sometimes get multiplexed.
		# BUG ALERT!  If -mount is the first option to find
		# under Unisys OS 6.0, then no files will be found, and
		# yet find will return successfully.  This can cause
		# VERY nasty things to happen!  10/11/91, DLB
		status "find $fs -mount, cpio"
		cd $fs
		echo "`pwd`:" 1>&2	# label cpio reports
		mkdir purgatory 2>/dev/null
		# Use -depth so list can be used in removal phase
		find . -depth -user $OUSER -mount -print |
		egrep -v "^\./purgatory/|^\./purgatory$|^\.$" |
		tee $FILELIST |	# save the list for later
		cpio -pdl $fs/purgatory || {
			status "find $fs -mount, cpio failed!"
			echo "find $fs -mount, cpio failed!" 1>&2
			PROCEED=false	# DO NOT REMOVE ANYTHING!
		}
		# The following will produce errors when users have
		# files in their directories not owned by themselves.
		# These messages will be collected in ERRLOG and
		# provided to the SA who must then decide how to handle
		# them.  Get the regular files first (-depth).  
		# BUG ALERT!  If -depth and -mount are reversed under
		# Unisys OS 6.0, then empty directories will not be
		# found!
		if test "$PROCEED" = "true"; then
		status "cat $fs FILELIST, rm[dir]"
		test "$DEBUG" = "true" && status "\t($FILELIST)"
		cat $FILELIST |	# This saves gobs of time
		awk 'BEGIN {print "# this prevents coredumps on empty input"}
		    {print "rm -f", $0, "2>/dev/null || rmdir", $0}' |
		/bin/sh
		fi # if PROCEED = true
		if test "$DEBUG" != "true"
		then rm $FILELIST
		fi
	done
	;; # simple case, find -mount option available

*) # Messy case, 2 "loops" necessary.
	# First, the easy cases:  everything but root.
	# Use file systems specified on command line, if any.  Else use
	# all mounted file systems.  
	for fs in ${FSS:-${ROOTS}}
	do
		case "$fs" in
		/) continue ;;	# skip root
		esac

		# Create list of Ouser's files, per fs, as a byproduct.
		FILELIST=/tmp/Ofiles.`/bin/basename $fs` export FILELIST
		PROCEED=true	# proceed w/the removals by default
		# Skip purgatories and the filesystem's root directory 
		# itself.  No need to skip FS_DUPL here because we are
		# finding from known filesystem roots.
		status "find $fs, cpio"
		cd $fs
		echo "`pwd`:" 1>&2	# label cpio reports
		mkdir purgatory 2>/dev/null
		find . -user $OUSER -print |
		egrep -v "^\./purgatory/|^\./purgatory$|^\.$" |
		tee $FILELIST |	# save the list for later
		cpio -pdl $fs/purgatory || {
			status "find $fs, cpio failed!"
			echo "find $fs, cpio failed!" 1>&2
			PROCEED=false	# DO NOT REMOVE ANYTHING!
		}
		# The following will produce errors when users have
		# files in their directories not owned by themselves.
		# Get the regular files first (-depth).  
		if test "$PROCEED" = "true"; then
		status "cat $fs FILELIST, rm[dir]"
		test "$DEBUG" = "true" && status "\t($FILELIST)"
		cat $FILELIST |	# This saves gobs of time
		awk 'BEGIN {print "# this prevents coredumps on empty input"}
		    {print "rm -f", $0, "2>/dev/null || rmdir", $0}' |
		/bin/sh
		fi # if PROCEED = true
		if test "$DEBUG" != "true"
		then rm $FILELIST
		fi
	done # easy cases, everything but root

	# Finally, the hard case: root without the -mount option.  Skip
	# purgatories.  Here there is no need to filter out /fjc (/app)
	# duplicates.  They were excluded from the ROOTFILES assignment.

	# Do root file system only if all file systems are implied 
	# (FSS is empty) or if / was among the command line specs.
	if test "$FSS" = "" ||
	    echo "$FSS" | egrep '^/ | / | /$|^/$'
	then # do root file system
	PROCEED=true	# proceed w/the removals by default
	status "find root, cpio"
	fs=/
	# Create list of Ouser's files, per fs, as a byproduct.
	FILELIST=/tmp/Ofiles.root export FILELIST
	cd $fs
	echo "`pwd`:" 1>&2	# label cpio reports
	mkdir purgatory 2>/dev/null
	# Note that ROOTFILES are expressed as full paths.  
	find $ROOTFILES -user $OUSER -print |
	egrep -v "^/purgatory/|^/purgatory$" |
	tee $FILELIST |	# save the list for later
	cpio -pdl $fs/purgatory || {
		status "find root, cpio failed!"
		echo "find root, cpio failed!" 1>&2
		PROCEED=false	# DO NOT REMOVE ANYTHING!
	}
	# The following will produce errors when users have files in 
	# their directories not owned by themselves.  Get the regular 
	# files first (-depth).  Note that ROOTFILES are expressed as 
	# full paths.  
	if test "$PROCEED" = "true"; then
	status "cat $fs FILELIST, rm[dir]"
	test "$DEBUG" = "true" && status "\t($FILELIST)"
	cat $FILELIST |	# This saves gobs of time
	awk 'BEGIN {print "# this prevents coredumps on empty input"}
	    {print "rm -f", $0, "2>/dev/null || rmdir", $0}' |
	/bin/sh
	fi # if PROCEED = true
	if test "$DEBUG" != "true"
	then rm $FILELIST
	fi
	fi # do root file system
	;; # messy case, no find -mount option
esac

# Since this script takes a long time, mail to $SA a completion
# message.  $SA should know that if no completion message is received,
# then ppurge didn't complete its job and needs to be investigated and
# probably run again.
mailx -s "$CMD $OUSER report" $SA < $ERRLOG
rm -f $ERRLOG
${ADM:-/adm}/ppremind $OUSER

# Condense the log record.  Leave only the begin and end records
# for $OUSER.  Only entries for $OUSER are touched.  This may leave 
# multiple "begin" entries.  
if test "$TESTING" = ""
then
	mkdir ${STATUSFILE}LCK && {
		sed "
		/^$OUSER|[^|]*|begin ${CMD}$/p
		/^$OUSER|[^|]*.*failed!$/p
		/^$OUSER|/d
		" $STATUSFILE > ${STATUSFILE}.tmp
		ln ${STATUSFILE}.tmp $STATUSFILE
		rm ${STATUSFILE}.tmp
		rmdir ${STATUSFILE}LCK ||
		    status "Could not remove ${STATUSFILE}LCK!"
	} || status "Could not lock and condense ${STATUSFILE}"
fi
status "done $CMD"
