#!/bin/ksh -h
# @(#) backoutpatch 5.0 95/09/07 SMI
# Exit Codes:	0	No error
#		1	Usage error
#		2	Attempt to backout a patch that hasn't been applied
#		3	Effective UID is not root
#		4	No saved files to restore
#		5	pkgrm failed
#		6	Attempt to back out an obsoleted patch
#		7	Attempt to restore CPIO archived files failed
#		8	Invalid patch id format
#		9	Prebackout script failed
#	   10	Postbackout script failed
#	   11	$SOFTINFO/INST_RELEASE file not found
#

# Set up the path to use with this script.

PATH=/usr/sbin:/usr/bin:$PATH
export PATH

umask 007

# Global Files

TMPSOFT=/tmp/soft.$$
ADMINFILE=/tmp/admin.$$

force=no
spoolonly="no"
pkginstlist=
pkglist=

ROOTDIR="/"
PATCHDB="/var/sadm/patch"
PKGDB="/var/sadm/pkg"
SOFTINFO="/var/sadm/softinfo"
CONTENTS="/var/sadm/install/contents"
TMP_LIB_DIR="/tmp/TmpLibDir.$$"
PKGDBARG=""
PARAMS_FILE=/tmp/ParamsFile.$$

PatchIdFormat='^[A-Z]*[0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9]$'

# Description:
#	Execute the prebackout script if it is executable. Fail if the
#	return code is not 0.
#
# Parameters:
#	$1	- package database directory
#	$2	- patch number
# Globals Set:
#	none
#
function execute_prebackout
{
	typeset -i retcode=0

	if [ -x $1/$2/prebackout ]
	then
		echo "Executing prebackout script..."
		$1/$2/prebackout
		retcode=$?
		if (( retcode != 0 ))
		then
			echo "prebackout patch exited with return code $retcode."
			echo "Backoutpatch exiting."
			exit 9
		fi
	fi
}

# Description:
#	Execute the postbackout script if it is executable. Fail if the
#	return code is not 0.
#
# Parameters:
#	$1	- package database directory
#	$2	- patch number
# Globals Set:
#	none
#
function execute_postbackout
{
	typeset -i retcode=0
	if [ -x $1/$2/postbackout ]
	then
		echo "Executing postbackout script..."
		$1/$2/postbackout
		retcode=$?
		if (( retcode != 0 ))
		then
			echo "postbackout patch exited with return code $retcode."
			echo "Backoutpatch exiting."
			exit 10
		fi
	fi
}

#
# Description:
#	Make internal variables available to child processes
#   of installpatch.  This is done by writing them to a
#   file and by exporting them.
# Parameters:
#	none
# Environment Variables Set:
#	none
#
function make_params_available
{
	echo "ROOTDIR=$ROOTDIR" >> $PARAMS_FILE
	echo "PATCHDB=$PATCHDB" >> $PARAMS_FILE
	echo "PKGDB=$PKGDB" >> $PARAMS_FILE
	echo "SOFTINFO=$SOFTINFO" >> $PARAMS_FILE
	echo "PKGDBARG=$PKGDBARG" >> $PARAMS_FILE

	echo "patchdir=$patchdir" >> $PARAMS_FILE
	echo "patchnum=$patchnum" >> $PARAMS_FILE
	echo "patchrev=$patchrev" >> $PARAMS_FILE

	export patchdir patchnum patchrev
	export PARAMS_FILE ROOTDIR PATCHDB PKGDB SOFTINFO PKGDBARG
}

# Description:
#	Give a list of applied patches similar in format to the showrev -p
#	command. Had to write my own because the showrev command won't take
#	a -R option.
#
# Parameters:
#	$1	- package database directory
# Globals Set:
#	none
#
function myshowrev
{
	olddir=$(pwd)
	cd $1
	patches=
	patches=$(sed -n 's/^SUNW_PATCHID=//p' ./*/pkginfo | sort -u)
	if [ "$patches" != "" ]
	then
		for apatch in $patches
		do
			outstr="Patch: $apatch Obsoletes: "
			patchvers=$(grep -l "SUNW_PATCHID=$apatch" ./*/pkginfo | \
					sed 's,^./\(.*\)/pkginfo$,\1,')
			obsoletes_printed="n"
			for vers in $patchvers
			do
				if [ "$obsoletes_printed" = "n" ]
				then
					outstr="$outstr$(sed -n 's/SUNW_OBSOLETES=//p' ./$vers/pkginfo) Packages: "
					outstr="$outstr$vers $(sed -n 's/VERSION=//p' ./$vers/pkginfo)"
					obsoletes_printed="y"
				else
					outstr="$outstr, $vers $(sed -n 's/VERSION=//p' ./$vers/pkginfo)"
				fi
			done
			echo $outstr
		done
	else
		echo "No patches are installed"
	fi
	cd $olddir
}


# Description:
#	Print out the usage message to the screen
# Parameters:
#	none
#
function print_usage
{
cat<<EOF

   Usage: backoutpatch [-f] [-V] [-R <root_path>] [-S <service>] <patch number>
 Options:
	 -f	force the backout regardless of whether the patch was
		superceded
	 -V	print version number only
     -R <root_path>
        Define the full path name of a subdirectory to use as the
        root_path. All package system information files are assumed
        to be located in a directory tree starting in the
        specified root_path. All patch files generated from the
        installpatch will be located in the same directory tree.
        Cannot be specified with the -S option
	 -S <service>
		Specify an alternate service (e.g. Solaris_2.3) for
		patch package processing references.

EOF
}

# Description:
#	Remove the patch files from the /export/root/templates directory.
#			NOT SUPPORTED AT THIS TIME
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
#	$3	- 'only spool' flag [ "yes" or "no" ]
#	$4	- softinfo directory
#	$5	- product version
# Globals Used:
#	TMPSOFT
#
#remove_spooled_files() {
#	cd $1/$2
#	if [ "$3" = "yes" ]; then
#		if [ -f softinfo_sed ]; then
#			sed -f softinfo_sed $4/$5 >$TMPSOFT
#			mv $4/$5 $4/sav.$5
#			cp $TMPSOFT $4/$5
#		fi
#	fi
#	if [ -f spooled_dirs ]; then
#		cat spooled_dirs | xargs rm -fr
#	fi
#	if [ "$3" = "yes" ]; then
#		exit 0
#	fi
#}

# Description:
#	Patch obsolecense message, printed if the patch being backed
#	out was superceded by other patches 
# Parameters:
#	$1	- patch ID
#	$2	- patch revision number
#
function print_obsolete_msg
{
	outstr="This patch was obsoleted by patch $1"
	if [ "$2" = "none" ]
	then
		outstr="$outstr."
	else
		outstr="$outstr-$2."
	fi
	echo $outstr
	echo "Patches must be backed out in the order in"
	echo "which they were installed. Patch backout aborted."
}

# Description:
#	Parse the arguments and set all affected global variables
# Parameters:
#	Arguments to backoutpatch
# Globals Set:
#	force
#	patchnum
#	ROOTDIR
#	PATCHDB
#	PKGDB
#	SOFTINFO
#	PKGDBARG
#	CONTENTS
#
function parse_args
{
	service_specified="n"
	rootdir_specified="n"
	while [ "$1" != "" ]
	do
		case $1 in
		-s)	spoolonly="yes"; shift;;
		-f)	force="yes"; shift;;
		-V)	echo "@(#) backoutpatch 5.0 95/09/07"
			exit 0 ;;
		-S)	shift
			if [ "$service_specified" != "n" ]
			then
				echo "Only one service may be defined."
				print_usage
				exit 1
			elif [ "$rootdir_specified" != "n" ]
			then
				echo "The -S and -R options are mutually exclusive."
				print_usage
				exit 1
			fi
			get_os_version "/var/sadm/softinfo"
			if [ "$1" != "$prodver" ]
			then
				if [ -d "/export/$1/var/sadm/pkg" ]
				then
					ROOTDIR=/export/$1
					PATCHDB=$ROOTDIR/var/sadm/patch
					PKGDB=$ROOTDIR/var/sadm/pkg
					SOFTINFO=$ROOTDIR/var/sadm/softinfo
					PKGDBARG="-R $ROOTDIR"
					CONTENTS=$ROOTDIR$CONTENTS
					service_specified="y"
				else
					echo "The $1 service cannot be found on this system."
					print_usage
					exit 1
				fi
			fi
			shift;;
		-R)	shift
			if [ "$rootdir_specified" != "n" ]
			then
				echo "Only one rootdir may be defined."
				print_usage
				exit 1
			elif [ "$service_specified" != "n" ]
			then
				echo "The -S and -R options are mutually exclusive."
				print_usage
				exit 1
			fi
			if [ -d "$1" ]
			then
				ROOTDIR=$1
				PATCHDB=$ROOTDIR/var/sadm/patch
				PKGDB=$ROOTDIR/var/sadm/pkg
				SOFTINFO=$ROOTDIR/var/sadm/softinfo
				PKGDBARG="-R $ROOTDIR"
				CONTENTS=$ROOTDIR$CONTENTS
				rootdir_specified="y"
			else
				echo "The $1 directory cannot be found on this system."
				print_usage
				exit 1
			fi
			shift;;
		-*)	print_usage; exit 1;;
		 *)	break;;
		esac
	done
	patchnum=$1
	#
	# If there is no patch number specified, exit with an error.
	#
	if [ "$patchnum" = "" ]
	then
		print_usage;
		exit 1
	fi
	echo "@(#) backoutpatch 5.0 95/09/07"
}

# Description:
#	Make sure the effective UID is '0'
# Parameters:
#	none
#
function validate_uid
{
	typeset -i uid=0
	uid=$(id | sed 's/uid=\([0-9]*\)(.*/\1/')
	if (( uid != 0 ))
	then
		echo "You must be root to execute this script."
		exit 3
	fi
}

# Description:
#	Get the product version <name>_<version> of local Solaris installation
# Parameters:
#	$1	- softinfo directory pathname
# Globals Set:
#	prodver
#
function get_os_version 
{
	if [ ! -f ${1}/INST_RELEASE ]
	then
		echo "$0 is unable to find the INST_RELEASE file.  This file"
		echo "must be present for $0 to function correctly."
		exit 11
	fi
	Product=
	Instver=
	Product=$(sed -n 's/^OS=\(.*\)/\1/p' $1/INST_RELEASE)
	Instver=$(sed -n 's/^VERSION=\(.*\)/\1/p' $1/INST_RELEASE)
	prodver=$Product"_"$Instver
}

# Description:
#	Build the admin script for pkgadd
# Parameters:
#	none
# Globals Used:
#	ADMINFILE

function build_admin
{
cat >$ADMINFILE <<EOF
mail=
instance=unique
partial=nocheck
runlevel=nocheck
idepend=quit
rdepend=quit
space=quit
setuid=nocheck
conflict=nocheck
action=nocheck
basedir=default
EOF
}

# Description:
# 	Restore old versions of files
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
#	$3	- package command relocation argument
#	$4	- path name of contents file
#
function restore_orig_files
{
	olddir=
	file=
	ownerfound=
	srch=
	cfpath=
	instlist=
	filelist=
	if [ ! -f $1/$2/.nofilestosave ]
	then
		echo "Restoring previous version of files..."
		olddir=$(pwd)
		cd $ROOTDIR
		# Must retain backwards compatibility to restore
		# archives which were not stored as files
		if [ -f $olddir/save/archive.cpio ]
		then 
			filelist=$(cat $olddir/save/archive.cpio | cpio -it 2>/dev/null)
			cpio -iumv -I $olddir/save/archive.cpio
		else 	if [ -f $olddir/save/archive.cpio.Z ]
		then
				filelist=$(zcat $olddir/save/archive.cpio.Z | cpio -it 2>/dev/null)
				zcat $olddir/save/archive.cpio.Z | cpio -iumv
			else
				filelist=$(find . -print | sed "s/^.//")
				find  . -print | cpio -pdumv
			fi
		fi
		if [ $? != 0 ]
		then
			echo "Restore of old files failed."
			echo "See README file for instructions."
			rm -f /tmp/*.$$
			remove_libraries
			exit 7
		fi
		echo "Making package database consistent with restored files:"
		rm -f /tmp/fixfile.$$ > /dev/null 2>&1
		for file in $filelist
		do
			if [ ! -f $file -o -h $file ]
			then
				continue
			fi
			# if file failed validation when the patch was 
			# installed, don't do an installf on it.  It should 
			# continue to fail validation after the patch is 
			# backed out.
			file1=$(expr $file : '\(\/.*\)')
			if [ "$file1" = "" ]
			then
				file1="/"$file
			fi
			srch="^$file1\$"
			if [ -f $olddir/.validation.errors ] && \
				grep "$srch" $olddir/.validation.errors >/dev/null 2>&1
			then 
				continue
			fi
			# The following commands find the file's entry in the
			# contents file, and return the first field of the 
			# entry. If the file is a hard link, the first field 
			# will contain an "=".  This will cause the -f test to 
			# fail and we won't try to installf the file.
			srch="^$file1[ =]"
			cfpath=$(grep "$srch" $CONTENTS | sed 's/ .*//')
			if [ "$cfpath" = "" -o ! -f "$ROOTDIR$cfpath" ]
			then
				continue
			fi
			ownerfound=no
			# Parsing pkgchk output is complicated because all text
			# may be localized. Currently the only line in the 
			# output which contains a tab is the line of packages 
			# owning the file, so we search for lines containing a 
			# tab.  This is probably reasonably safe. If any of the
			# text lines end up with tabs due to localization, the 
			# pkginfo check should protect us from calling installf
			# with a bogus package instance argument.
			pkgchk $3 -lp $file1 | grep '	' | \
			while read instlist
			do
				for i in $instlist
				do
					pkginfo $3 $i >/dev/null 2>&1
					if [ $? = 0 ]
					then
						echo $i $file1 >> /tmp/fixfile.$$
						ownerfound=yes
						break
					fi
				done
				if [ $ownerfound = "yes" ]
				then
					break
				fi
			done
		done
		if [ -s /tmp/fixfile.$$ ]
		then
			sed 's/^\([^ ]*\).*/\1/' /tmp/fixfile.$$ | sort -u | \
			while read pkginst
			do
				grep "^${pkginst} " /tmp/fixfile.$$ | \
				sed 's/^[^ ]* \(.*\)/\1/' | \
				if [ "$ROOTDIR" != "/" ]
				then
					installf $PKGDBARG $pkginst -
					installf $PKGDBARG -f $pkginst
				else
					installf $pkginst -
					installf -f $pkginst
				fi
			done
		fi
		cd $olddir
	fi
}

#
# Description:
#	Change directory to location of patch
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
# Globals Set:
#	patchdir
#	patchid
#	patchrev
#
function activate_patch
{
	echo $patchnum | grep $PatchIdFormat >/dev/null
	if [ "$?" != "0" ]
	then
		echo "Invalid patch id format: $patchnum"
		exit 8
	fi

	if myshowrev $PKGDB | grep -s "^Patch:[ 	]*$2" > /dev/null 2>&1
	then
		patchdir=$1/$2
		cd $patchdir
		patchid=$(expr $2 : '\(.*\)-.*')
		patchrev=$(expr $2 : '[^-]*-\(.*\)')
	else
		echo "Patch $2 has not been successfully applied to this system."
 		if [ -d $1/$2 ]
		then
 			echo "Will remove directory $1/$2"
 			rm -r $1/$2
 		fi
		exit 2
	fi
}

# Description:
#	Find the package instances for this patch
# Parameters:
#	$1	- package database directory
#	$2	- patch number
# Globals Set:
#	pkginstlist
#
function get_pkg_instances
{
	pkginst=
	j=
	for j in $1/*
	do
		if grep -s "SUNW_PATCHID *= *$2" $j/pkginfo > /dev/null 2>&1
		then
			pkginst=$(basename $j)
			pkginstlist="$pkginstlist $pkginst"
		fi
	done
}

# Description:
# 	Check to see if this patch was obsoleted by another patch.
# Parameters:
#	$1	- patch database directory
#	$2	- patch ID
#	$3	- patch revision
#
function check_if_obsolete
{
	Patchid=
	oldbase=
	oldrev=
	opatchid=
	obase=
	obsoletes=
	i=
	j=
	if [ -d $1 ]
	then
		cd $1
		for i in * X
		do
			if [ $i = X -o "$i" = "*" ]
			then
				break
			elif [ ! -d $i ]
			then
				continue
			fi
			cd $i
			for j in */pkginfo X
			do
				if [ "$j" = "X" -o "$j" = "*/pkginfo" ]
				then
					break
				fi
				Patchid=$(sed -n 's/^[ 	]*SUNW_PATCHID[ 	]*=[ 	]*\([^ 	]*\)[	 ]*$/\1/p' $j)
				if [ "$Patchid" = "" ]
				then
					continue
				fi
				oldbase=$(expr $Patchid : '\(.*\)-.*')
				oldrev=$(expr $Patchid : '.*-\(.*\)')
				if [ $oldbase = $2 -a $3 -lt $oldrev ]
				then
					print_obsolete_msg "$2" "$oldrev"
					exit 6
				fi
				obsoletes=$(sed -n 's/^[	 ]*SUNW_OBSOLETES[ 	]*=[ 	]*\([^ 	]*\)[ 	]*$/\1/p' $j)
				while [ "$obsoletes" != "" ]
				do
					opatchid=$(expr $obsoletes : '\([0-9\-]*\).*')
					obsoletes=$(expr $obsoletes : '[0-9\-]*[ ,]*\(.*\)')
					# patchrevent infinite loop.  If we couldn't
					# find a valid patch id, just quit.
					if [ "$opatchid" = "" ]
					then
						break;
					fi
					obase=$(expr $opatchid : '\(.*\)-.*')
					if [ "$obase" = "" ]
					then
						# no revision field in opatchid,
						# might be supported someday 
						# (we don't use the revision 
						# field for obsoletion testing)
						obase=$opatchid
					fi
					if [ $obase = $2 -a $2 != $oldbase ]
					then
						print_obsolete_msg "$2" "none"
						exit 6
					fi
				done
			done
			cd ..
		done
	fi
}

# Description:
#	Check to see if originally modified files were saved. If not,
#	the patch cannot be backed out.
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
#
function check_if_saved
{
	if [ ! -f $1/$2/.oldfilessaved -a \
		 ! -f $1/$2/.nofilestosave ]
	then
		echo "Patch $2 was installed without backing up the original"
		echo "files. It cannot be backed out."
		exit 4
	fi
}

# Description:
#	Get the list of packages 
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
# Globals Set:
#	pkglist
#
function get_package_list
{
	pkg=
	i=
	cd $1/$2
	for i in */pkgmap
	do
		pkg=$(expr $i : '\(.*\)/pkgmap')
		pkglist="$pkglist $pkg"
	done
}

# Description:
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
#	$3	- softinfo directory
#	$4	- product version
# Globals Used:
#	TMPSOFT
#
function cleanup
{
	cd $1
	rm -f /tmp/*.$$
	if [ -f softinfo_sed ]
	then
		sed -f softinfo_sed $3/$4 > $TMPSOFT
		mv $3/$4 $3/sav.$4
		cp $TMPSOFT $3/$4
	fi
	rm -fr ./$2/*
	rm -fr $2
}

# Description:
#	Remove appropriate patch packages from the system 
#	NOTE: this will not restore the overwritten or removed files, but will
#	      remove any files which were added by the patch.
# Parameters:
#	$1	- patch database directory
#	$2	- patch number
#	$3	- packaging command relocation argument 
# Globals Used:
#	ADMINFILE
#	pkginstlist
#
function remove_patch_pkgs
{
	logfile=/tmp/pkgrmlog.$$
	pkgrmerr=
	i=
	for i in $pkginstlist
	do
		echo "\nRemoving patch package for $i:"
		pkgrm $3 -a $ADMINFILE -n $i>$logfile 2>&1
		pkgrmerr=$?
		cat $logfile >>$1/$2/log
		cat $logfile | grep -v "^$"
		rm -f $logfile
		if [ $pkgrmerr != 0 -a $pkgrmerr != 2 -a \
			$pkgrmerr != 10 -a $pkgrmerr != 20 ]
		then
			echo "\npkgrm of $i package failed with return code $pkgrmerr."
			echo "See $1/$2/log for reason for failure."
			rm -fr /tmp/*.$$
			remove_libraries
			exit 5
		fi
	done
}

# Description:
#	Copy required libraries to TMP_LIB_DIR, set and
#	export LD_PRELOAD.
# Parameters:
#	none
# Environment Variables Set:
#	LD_PRELOAD
#
function move_libraries
{
	typeset -i Rev=0

    Rev=$(echo $Instver | sed -e 's/[0-9]\.//')
    if (( Rev >= 5 ))
    then
        if [ ! -d $TMP_LIB_DIR ]
        then
            mkdir -p -m755 $TMP_LIB_DIR
        fi
        LD_PRELOAD=
        for Lib in libc libdl libelf libintl libw libadm
        do
            cp /usr/lib/${Lib}.so.1 ${TMP_LIB_DIR}/${Lib}.so.1
            chown bin ${TMP_LIB_DIR}/${Lib}.so.1
            chgrp bin ${TMP_LIB_DIR}/${Lib}.so.1
            chmod 755 ${TMP_LIB_DIR}/${Lib}.so.1
            LD_PRELOAD="${LD_PRELOAD} ${TMP_LIB_DIR}/${Lib}.so.1"
        done
        export LD_PRELOAD
    fi
}

# Description:
#	remove the TMP_LIB_DIR directory
# Parameters:
#	none
# Environment Variables Set:
#	LD_PRELOAD
#
function remove_libraries
{
	LD_PRELOAD=
	export LD_PRELOAD
	rm -rf $TMP_LIB_DIR
}

#########################################################
#							#
# 			Main routine			#
#							#
#########################################################

# -	Parse the argument list and set globals accordingly
# -	Make sure the user is running as 'root'
# -	Get the product version <name>_<version> of the local
#	Solaris installation
# - 	activate the patch

echo

parse_args $*

validate_uid

get_os_version "$SOFTINFO"

activate_patch "$PATCHDB" "$patchnum"

make_params_available

#
# Check to see if this patch was obsoleted by another patch
#
if [ "$force" = "no" ]
then
	check_if_obsolete "$PATCHDB" "$patchid" "$patchrev"
fi

execute_prebackout "$PATCHDB" "$patchnum"

# -	Remove spooled files under /export/root/templates (NOT SUPPORTED)
# remove_spooled_files "$PATCHDB" "$patchnum" "$spoolonly" "$SOFTINFO" "$prodver"

# -	Check to see if original files were actually saved
# -	Generate list of packages to be removed
# -	Find the package instances for this patch
# -	Build admin file for later use by pkgrm
# -	pkgrm patch packages
# -	Restore the original files which were overwritten by the patch
# -	Update the prodver file & cleanup tmp files

check_if_saved "$PATCHDB" "$patchnum"

get_package_list "$PATCHDB" "$patchnum"

get_pkg_instances "$PKGDB" "$patchnum"

build_admin

trap 'remove_libraries' HUP INT QUIT TERM
move_libraries

remove_patch_pkgs "$PATCHDB" "$patchnum" "$PKGDBARG"

restore_orig_files "$PATCHDB" "$patchnum" "$PKGDBARG" "$CONTENTS"

remove_libraries

execute_postbackout "$PATCHDB" "$patchnum"

cleanup "$PATCHDB" "$patchnum" "$SOFTINFO" "$prodver"

echo "Patch $patchnum has been backed out."

exit 0
