#!/bin/bash

###########################################################################
#
#	Shell program to download updates for RH Linux from an FTP site.
#
#	Copyright  2000-2002, William Shotts, Jr.
#	<bshotts@users.sourceforge.net>
#
#	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. 
#
#	This software is part of the LinuxCommand.org project, a site for
#	Linux education and advocacy devoted to helping users of legacy
#	operating systems migrate into the future.
#
#	You may contact the LinuxCommand.org project at:
#
#		http://www.linuxcommand.org
#
#	Description:
#
#	This program is used to get update and errata files from an
#	ftp site (presumably a mirror of Red Hat's own site).  In
#	addition to downloading RPM files, this program will also
#	report on the status of the system compared to the contents
#	of the incoming directory (assumed to be under /usr/local/lib).
#	By using this feature you can determine which downloaded RPM
#	files need to be installed.
#
#	See the function retrieve_files (below) for constants used to
#	define the name of the ftp site and the source directory on the
#	remote site. 
#
#	NOTE: You must be the superuser to run this script since you will
#	likely wish to download the RPM files into a directory that is
#	not world writable (such as /usr/local/lib).
#
#	Usage:
#
#		rh-errata -v ver [-i] [-d dir] [-k kern] [-m user] [-n attempts]
#
#		rh-errata -v ver -r [-d dir] [-m user]
#
#		rh-errata -v ver -c [-d dir] [-m user]
#
#		rh-errata -h | --help
#
#	Options:
#
#		-v ver		Get updates for version "ver".
#		-i		Intelligent mode
#		-d dir		Set root directory for download.
#				Defaults to "/usr/local/lib".
#		-k kernel	Kernel architecture "kern"
#		-m user		Mail results to "user".
#		-n attempts	Retry downloads "attempts" times.
#		-r		Report which updates need to be applied.
#		-c		Clean repository of obsolete and corrupt
#				packages.
#		-h, --help	Display this help message and exit.
#
#	Examples:
#
#		rh-errata -v 6.0
#
#			Retrieve all RPM files for version 6.0 that have not
#			already been downloaded. Use this method if you wish
#			to maintain a complete mirror of updates.  If the
#			incoming directory /usr/local/lib/RH6.0-errata does
#			not exist, the user will be prompted to create it.
#
#		rh-errata -v 6.0 -n 50
#
#			Like above, except attempt download 50 times with a
#			one minute delay between attempts.  This is good for
#			getting updates from busy sites.
#
#		rh-errata -v 6.2 -i
#
#			Retrieve RPM files for version 6.2 using "intelligent
#			mode".  In this mode, rh-errata will only download
#			packages that are installed on the machine running
#			rh-errata.  The intelligent mode also sets the kernel
#			architecture (overriding the -k option) automatically
#			from the value return from the "arch" command.  Use
#			intelligent mode if you are only maintaining a single
#			machine.  If the incoming directory
#			/usr/local/lib/RH6.2-errata does not exist, the user
#			will be prompted to create it.
#
#		rh-errata -v 5.2 -r -m root
#
# 			Check which files in the 5.2 directory need to be
#			installed and mail the report to the superuser.  If
#			the incoming directory /usr/local/lib/RH5.2-errata
#			does not exist, the script will terminate with an
#			error.
#
#		rh-errata -v 6.2 -c
#
#			The "-c" option performs just like the "-r" option
#			except that any package that is obsolete (meaning that
#			there is newer package already installed) or corrupt
#			(meaning that it fails the md5 checksum test) is 
#			deleted from the respository.  This option is useful
#			for cleaning out old stuff that will build up over time.
#
#		rh-errata -v 6.0 -k i686 -d $HOME/rpms
#
#			Retrieve RPM files for version 6.0 and kernel 
#			architecture i686 and place them in
#			$HOME/rpms/RH6.0-errata rather than the default
#			directory /usr/local/lib/RH6.0-errata.  If the
#			directory $HOME/rpms does not exist, the script will
#			terminate with an error.
#						
#	Revisions:
#
#	01/02/2000	File created
#	01/11/2000	Fixed typo
#	03/04/2000	Changed default ftp site to ftp.freesoftware.com
#       03/18/2000      Changed default ftp site to ftp.valinux.com
#	06/27/2000	Added support for kernel architecture and noarch
#	07/04/2000	Changed ftp site back to ftp.freesoftware.com
#	07/21/2000	Changed ftp site back to ftp.valinux.com
#	07/23/2000	Added *UNKNOWN* package status indicating that
#			rpm crashed during an inquiry.  I see this problem
#			on my 5.2 box. (1.0.1)
#	11/23/2000	Added "intelligent mode" and made misc cleanups.
#			(1.1.0)
#	03/30/2001	Updated VA Linux ftp site path (1.1.1)
#	04/01/2001	Added -c option to remove obsolete and corrupt
#			packages from the respository. (1.2.0)
#	09/29/2001	Changed default ftp site to ftp1.sourceforge.net
#			as previous default is no longer available. (1.2.1)
#	01/12/2002	Changed default ftp site to distro.ibiblio.org
#			as previous default is no longer available (1.2.2)
#	02/09/2002	Cosmetic fixes. (1.2.3)
#	07/25/2002	Added -n option for multiple download attempts
#			(1.2.4)
#
#	$Id: rh-errata,v 1.7 2002/07/28 12:50:08 bshotts Exp $
###########################################################################


###########################################################################
#	Constants
###########################################################################

# Also see function retrieve_files (below) for constants specific to the
# ftp site

PROGNAME=$(basename $0)
VERSION="1.2.4"
if [ -d ~/tmp ]; then
	TEMP_DIR=~/tmp
else
	TEMP_DIR=/tmp
fi
DEFAULT_INCOMING_ROOT=/usr/local/lib	# change this if desired
PLATFORM=i386				# change this if platform != Intel
ATTEMPT_DELAY=60
TEMP0=${TEMP_DIR}/${PROGNAME}_0.$$.$RANDOM
TEMP1=${TEMP_DIR}/${PROGNAME}_1.$$.$RANDOM
TEMP2=${TEMP_DIR}/${PROGNAME}_2.$$.$RANDOM
TEMP3=${TEMP_DIR}/${PROGNAME}_3.$$.$RANDOM


###########################################################################
#	Functions
###########################################################################


function clean_up
{

	#####	
	#	Function to remove temporary files and other housekeeping
	#	No arguments
	#####

	rm -f ${TEMP0} ${TEMP1} ${TEMP2} ${TEMP3}
}


function graceful_exit
{
	#####
	#	Function called for a graceful exit
	#	No arguments
	#####

	clean_up
	exit
}


function error_exit 
{
	#####	
	# 	Function for exit due to fatal program error
	# 	Accepts 1 argument
	#		string containing descriptive error message
	#####

	local err_msg
	
	err_msg="${PROGNAME}: ${1}"
	echo ${err_msg} >&2
	clean_up
	exit 1
}


function term_exit
{
	#####
	#	Function to perform exit if termination signal is trapped
	#	No arguments
	#####

	echo "${PROGNAME}: Terminated"
	clean_up
	exit
}


function int_exit
{
	#####
	#	Function to perform exit if interrupt signal is trapped
	#	No arguments
	#####

	echo "${PROGNAME}: Aborted by user"
	clean_up
	exit
}


function usage
{
	#####
	#	Function to display usage message (does not exit)
	#	No arguments
	#####

	echo "Usage:"
	echo "	${PROGNAME} -v version [-i] [-d dir] [-k kernel] [-m user] [-n attempts]"
	echo "	${PROGNAME} -v version -r [-d dir] [-m user]"
	echo "	${PROGNAME} -v version -c [-d dir] [-m user]"
	echo "	${PROGNAME} -h | --help"
}


function helptext
{
	#####
	#	Function to display help message for program
	#	No arguments
	#####
	
	local tab=$(echo -en "\t\t")
		
	cat <<- -EOF-
	
	${PROGNAME} ver. ${VERSION}	
	This program downloads updates for RH Linux from an FTP site.
	
	$(usage)
	
	Options:
	
	-v ver		Get updates for "version".
	-i		Intelligent mode
	-d dir		Set root directory for download.
			${tab}Defaults to "/usr/local/lib".
	-k kernel	Kernel platform (example i586)
	-m user		Mail results to "user".
	-n attempts	Retry downloads "attempts" times.
	-r		Report which updates need to be applied.
	-c		Clean respository of obsolete and
			${tab}corrupt packages.
	-h, --help	Display this help message and exit.
	
			
	NOTE: You must be the superuser to run this script.
		
	-EOF-
}	


function root_check
{
	#####
	#	Function to check if user is root
	#	No arguments
	#####
	
	if [ "$(id | sed 's/uid=\([0-9]*\).*/\1/')" != "0" ]; then
		error_exit "You must be the superuser to run this script."
	fi
}


function package_name
{
	#####
	#	Given an RPM file name, returns package name
	#	Arguments:
	#		1	RPM file name (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "package_name: missing argument 1"
	fi

	echo $1 | awk '
	
		BEGIN {
			FS = "-"
		}
		
		NF == 3 {
			print $1
		}
		
		NF > 3 {
			printf("%s", $1)
			for (i = 2; i <= ( NF - 2 ); i++) {
				printf("-%s", $i)
			}
			print ""
		}
	'
}	# end of package_name


function package_needed
{
	#####
	#	Returns 0 if package is installed on system
	#	Arguments:
	#		1	package name (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "package_needed: missing argument 1"
	fi

	rpm -q $1 2>&1
	return $?
	
}	# end of package_needed


function check_rpm_integrity
{
	#####
	#	Returns 0 if RPM package file is not corrupted
	#	Arguments:
	#		1	package file name (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "check_rpm_integrity: missing argument 1"
	fi

	# Note: this assumes rpm version > 3.  To use with earlier versions
	# remove "--nogpg"
	
	rpm --checksig --nopgp --nogpg $1 2>&1
	return $?

}	# end of check_rpm_integrity


function installed_package_date
{
	#####
	#	Returns build time of an installed package
	#	Arguments:
	#		1	package name (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "installed_package_date: missing argument 1"
	fi

	# Get build time from rpm database - notice "tail" trick to handle
	# when more than one instance of a package is installed.  I assume
	# the last one listed is the most recent.
	
	rpm -q $1 --queryformat "%{BUILDTIME}\n" | tail --lines=1

}	# end of installed_package_date


function file_package_date
{
	#####
	#	Returns build time of a package file
	#	Arguments:
	#		1	package file name (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "file_package_date: missing argument 1"
	fi

	rpm -qp $1 --queryformat "%{BUILDTIME}\n"

}	# end of file_package_date


function newest_packages
{
	#####
	#	Generate list of of newest packages
	#	Arguments:
	#		1	Input file (required)
	#		2	Output file (required)
	#####

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "newest_packages: missing argument 1"
	fi
	if [ "$2" = "" ]; then 
		error_exit "newest_packages: missing argument 2"
	fi

	local input_file=$1
	local output_file=$2
	local curr_pkg=
	local next_pkg=
	local curr_file=
	
	# Sort input file
	
	sort $input_file > $output_file
	cp $output_file $input_file
	> $output_file
	
	# Find newest assuming (perhaps wrongly) that newest sorts higher
	
	for i in $(cat $input_file); do
		next_pkg=$(package_name $i)
		if [ "$next_pkg" != "$curr_pkg" ]; then
			if [ "$curr_file" != "" ]; then
				echo $curr_file >> $output_file
			fi
			curr_pkg=$next_pkg
		fi
		curr_file=$i
	done
	echo $curr_file >> $output_file
	
}	# end of newest_packages


function generate_report
{
	#####
	#	Reports which rpms need to be installed
	#	also removes obsolete and corrupted rpms
	#	if $flag_clean = 1
	#
	#	Arguments:
	#		1	RH Linux version (required)
	#		2	Incoming root directory (required)
	#####

	local version=$1
	local incoming_root=$2
	local INCOMINGDIR=${incoming_root}/RH${version}-errata
	local i
	local status
	local file_time
	local package_time
	local pkg_name

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "generate_report: missing argument 1"
	fi

	if [ ! -d ${INCOMINGDIR} ]; then
		error_exit "generate_report: no such version directory on system"
	fi
	
	cd ${INCOMINGDIR} || error_exit "generate_report: cannot change to incoming directory"

	echo -e "\nPackage Status Report - $(date)"
	echo -e "Version: ${version}\n\n"
	
	for i in *.rpm ; do
		status="*UNKNOWN*"
		if check_rpm_integrity $i > /dev/null; then
			pkg_name=$(package_name $i)
			if package_needed $pkg_name > /dev/null; then
				file_time=$(file_package_date $i)
				package_time=$(installed_package_date $pkg_name)
				if [ "$file_time" -gt "$package_time" ]; then
					status="*INSTALLATION NEEDED*"
				fi
				if [ "$file_time" -lt "$package_time" ]; then
				
					# If -c option set, delete obsolete package
					
					if [ $flag_clean = "1" ]; then
						rm $i
						status="*OBSOLETE PACKAGE DELETED*"
					else
						status="Obsolete"
					fi
				fi
				if [ "$file_time" -eq "$package_time" ]; then
					status="Already installed"
				fi
			else
				status="Not needed"
			fi
		else
			if [ $flag_clean = "1" ]; then
				rm $i
				status="*CORRUPT PACKAGE DELETED*"
			else
				status="*PACKAGE CORRUPT*"
			fi
		fi
		printf "%-45s: %s\n" $i "$status"
	done

}	# end of generate_report


function retrieve_files
{
	#####
	#	Downloads files from FTP site
	#	Arguments:
	#		1	RH Linux version (required)
	#		2	platform/kernel such as i386 (required)
	#		3	incoming root directory (required)
	#####

	local version=$1
	local platform=$2
	local incoming_root=$3

	# These constants are specific to the ftp site used.
	# Edit as needed for your chosen site.
	
	local HOST=distro.ibiblio.org
	local TARGET=/pub/Linux/distributions/redhat/updates/${version}/en/os/${platform}
	local USRNAME=anonymous
	local PASSWD=${USER}@${HOSTNAME}

	local INCOMINGDIR=${incoming_root}/RH${version}-errata
	local file_count=0
	local pkg_name=
	local attempt_count=0
	local es=

	# Fatal error if required arguments are missing

	if [ "$1" = "" ]; then 
		error_exit "retrieve_files: missing argument 1"
	fi

	if [ "$2" = "" ]; then 
		error_exit "retrieve_files: missing argument 2"
	fi

	if [ "$3" = "" ]; then 
		error_exit "retrieve_files: missing argument 3"
	fi
	
	# Create temporary files

	> ${TEMP0}
	> ${TEMP1}
	> ${TEMP2}
	> ${TEMP3}

	# Change to the directory where incoming files will be written

	if [ -d ${INCOMINGDIR} ] ; then
		cd ${INCOMINGDIR} || error_exit "retrieve_files: Incoming directory not available!"
	else
		echo -n "Incoming directory ${INCOMINGDIR} does not exist. Create? [y/n]: "
		read foo
		case $foo in
			y|Y )	mkdir ${INCOMINGDIR} || error_exit "retrieve_files: Cannot create incoming directory!"
				cd ${INCOMINGDIR}
				;;
			*)	error_exit "retrieve_files: No incoming directory available - aborting!"
				;;
		esac
	fi


	# Use ftp to get a directory of the target (remote) directory
	
	while [ $attempt_count -lt $max_attempts ]; do
		echo -e "\nGetting directory from ${HOST}:${TARGET}..."
		echo	"user $USRNAME $PASSWD
			prompt off
			binary
			cd ${TARGET}
			dir *.${platform}.rpm  ${TEMP0}
			bye" | ftp -n ${HOST}

		# Strip off extra stuff from directory listing

		if [ -s ${TEMP0} ]; then
			awk '
				NF==9 && !( $1 ~/^d/ ) {
					print $9
				}

	 		' < ${TEMP0} > ${TEMP1}
			file_count=$(wc -l ${TEMP1} | awk '{ print $1}')
			attempt_count=0
			break
		else
			# If directory file is empty, something went wrong
			
			attempt_count=$((attempt_count + 1))
			if [ $attempt_count -lt $max_attempts ]; then
				echo "Attempt $attempt_count failed, waiting $ATTEMPT_DELAY seconds before retry."
				sleep $ATTEMPT_DELAY
			fi
		fi
	done

	if [ $attempt_count -gt 0 ]; then
		error_exit "retrieve_files: Unable to get directory from ftp server"
	fi
	echo "$file_count files available."

	# Determine which files from the target are needed.

	case $intell_mode in
		NO )	for i in $(cat ${TEMP1}); do
		
				# Check if the file is already in respository
				
				if [ ! -f ${INCOMINGDIR}/${i} ]; then
					echo $i >> ${TEMP3}
					
				else	# Check if existing file in respository is corrupt
				
					if check_rpm_integrity $i > /dev/null; then
						:
					else
						echo $i >> ${TEMP3}
					fi
				fi
			done
			;;
			
		YES )	for i in $(cat ${TEMP1}); do
		
				# Get name of package from RPM database
				
				pkg_name=$(package_name $i)
				
				# Check if file is already in respository
				
				if [ ! -f ${INCOMINGDIR}/${i} ]; then
				
					# Find out if package is installed (i.e. needed)
					
					if package_needed $pkg_name > /dev/null; then
						echo $i >> ${TEMP2}
					fi
					
				else	# Check if existing file in respository is corrupt
				
					if check_rpm_integrity $i > /dev/null; then
						:
					else
						echo $i >> ${TEMP2}
					fi
				fi
			done

			# Find the newest versions of the needed packages
			
			if [ -s $TEMP2 ]; then
				newest_packages $TEMP2 $TEMP3
			fi
			;;
						
		* )	error_exit "retrieve_files: invalid retrieval mode"
	esac
	
	file_count=$(wc -l ${TEMP3} | awk '{ print $1}')
	
	# Check if any files need retrieval
		
	if [ ! -s ${TEMP3} ]; then
		echo "No new files to retrieve."
		return 0
	else
		echo -e "$file_count files needed."
		echo -e "\nAttempting to retrieve the following files:"
		cat ${TEMP3}
	fi

	# Get the files

	echo -e "\nRetrieving files from ${HOST}:${TARGET}..."
	date
	
	# Generate ftp commands and retrieve files

	attempt_count=0
	while [ $attempt_count -lt $max_attempts ]; do
		es=0
		awk -v USER=${USRNAME} -v PASS=${PASSWD} -v TARGET=${TARGET} '

			BEGIN {
				print "user " USER " " PASS
				print "prompt off"
				print "binary"
				print "cd " TARGET
			}

			{
				print "get " $1
			}
	
			END {
				print "bye"
			}

		' < ${TEMP3} | ftp -n ${HOST}

		# Check that all the files were retrieved

		for i in $(cat $TEMP3); do
			if [ ! -r $i ]; then
				attempt_count=$((attempt_count + 1))
				es=1
				if [ $attempt_count -lt $max_attempts ]; then
					echo "Attempt $attempt_count failed, waiting $ATTEMPT_DELAY seconds before retry."
					sleep $ATTEMPT_DELAY
					break
				fi
			fi
		done

		# If retrieval was sucessful, break out of loop

		if [ $es -eq 0 ]; then
			attempt_count=0
			break
		fi
	done

	# Die if we exceeded number of attempts without success

	if [ $attempt_count -ne 0 ]; then
		 error_exit "retrieve_files: Error during file retrieval"
	fi
	echo -e "\nRetrieval complete. $(date)"

}	# end of retrieve_files



###########################################################################
#	Program starts here
###########################################################################

# Set file creation mask so that all files are created with 600 permissions.
# This will help protect temp files.

umask 066
root_check

# Trap TERM, HUP, and INT signals and properly exit

trap term_exit TERM HUP
trap int_exit INT

# Process command line arguments

if [ "$1" = "--help" ]; then
	helptext
	graceful_exit
fi

flag_mail=0
flag_report=0
flag_clean=0
version=
addressee=
incoming_root=$DEFAULT_INCOMING_ROOT
kernel=$PLATFORM
intell_mode=NO
max_attempts=1

while getopts ":v:d:m:k:n:rchi" opt; do
	case $opt in
		v )	version=${OPTARG}
			;;
		d )	incoming_root=${OPTARG}
			;;
		m )	flag_mail=1
			addressee=${OPTARG}
			;;
		n )	max_attempts=${OPTARG}
			;;
		k )	kernel=${OPTARG}
			;;
		r )	flag_report=1
			;;
		i )	intell_mode=YES
			kernel=$(arch)
			;;
		c )	flag_clean=1
			flag_report=1
			;;
		h )	helptext
			graceful_exit
			;;
		* )	usage
			exit 1
			;;
	esac
done

# Version must be set

if [ "$version" = "" ]; then
	error_exit "Red Hat version must be specified! Try '-h' for help."
fi
if [ "$(echo $version | awk '/^[0-9][.][0-9]$/ { print $0 }')" = "" ]; then
	error_exit "Invalid version number format.  Must be in form 'n.n'"
fi

# If major Red Hat version is less than 6, then kernel arch is not supported

if [ $(echo $version | cut -d "." -f 1) -lt 6 ]; then
	kernel=$PLATFORM
fi

# Check that incoming root directory exists

if [ ! -d "$incoming_root" ]; then
	error_exit "Directory $incoming_root does not exist."
fi

# Generate report if "-r" argument passed, otherwise retrieve files

if [ $flag_report = 1 ]; then
	if [ $flag_mail = 1 ]; then
		generate_report $version $incoming_root | mail -s "Package report from ${PROGNAME}" $addressee
	else
		generate_report $version $incoming_root
	fi
else
	if [ $flag_mail = 1 ]; then
		( if [ $kernel != $PLATFORM ]; then
			retrieve_files $version $kernel $incoming_root
		fi
		retrieve_files $version $PLATFORM $incoming_root
		retrieve_files $version noarch $incoming_root) | mail -s "Retrievial report from ${PROGNAME}" $addressee
	else
		if [ $kernel != $PLATFORM ]; then
			retrieve_files $version $kernel $incoming_root
		fi
		retrieve_files $version $PLATFORM $incoming_root
		retrieve_files $version noarch $incoming_root
	fi
fi

graceful_exit


syntax highlighted by Code2HTML, v. 0.9.1