#!/bin/sh
#
#set -x
# @(#)installpatch	1.5 05/24/07
#
# Copyright 2007 Sun Microsystems, Inc. All Rights Reserved
#
# installpatch [-y] [-i] [-V] [-h] [<patch>]
#          -y: Automatically answer 'y' to all questions
#          -i: Force installation of patch rpms even if no version of
#              the rpm is already installed on the system. The default
#              behavior is to only freshen previously installed rpms
#          -V: Print version then exit
#          -h: Print usage then exit
#     <patch>: Path to the directory containing the patch. If ommitted
#              assumes current directory.
#
# Installs (freshens) a set of "patch" rpms onto the system. The patch rpms
# are picked up from the directory specified by <patch> (or the current
# directory if <patch> is not specified). If a version of the rpm is not
# already installed on the system, then the patch rpm won't be applied.
# (i.e. it uses the RPM "freshen" semantics) -- the exception to this is
# if the "-i" option is used.
#
# The <patch> directory is assumed to contain the patch rpms plus an
# option 'rpmlist.txt' file that lists the order that the patch rpms
# should be installed.
#
# This utility handles the case when the existing (already installed) rpm
# has been relocated via '--prefix'. In this case the update is applied using
# the same prefix as the installed rpm. The simplest way to handle this
# robustly is to install each rpm one at a time (since different rpms may
# have different prefixes). And this script does just that. The downside
# to this is that you don't get the package dependency resolution benefit
# of when you add/update all rpm's with one call to rpm(8). Therefore
# you may find it necessary to add "--nodeps" to the arguments passed
# to rpm(8) (see _rpm_freshen_args below)
#
# This utility detects, but does not handle the case of multiple instances
# of a given rpm installed on the system. If this situation is detected
# then installpatch exits without modifying the system.
#
# Inputs:
#
#          <patch>/*.rpm: Patch rpm files to update the system with
#
#    <patch>/rpmlist.txt: Optional. List of rpms to install in the order they
#                         should be installed in. One file name per line.
#                         If this file does not exist then all *.rpm files
#                         in the <patch> directory are used in unspecified
#                         oprder.
#
# To use this script in your Linux patch:
#
# 1. Copy this script to your patch directory. I.e. the same directory that
#    contains your patch rpms.
#
# 2. Create a file named 'rpmlist.txt' in the same directory as this script.
#    List the rpm files from your patch in the order you would like them
#    installed. If rpm order does not matter then you may omit this file
#    and installpatch will pick up all *.rpm files from the <patch> directory.
#
# 3. Tweak _rpm_freshen_args (and _rpm_install_args) below if you need to add
#     something like "--noscripts" or "--nodeps"
#
# Exit Codes:
#               0       No error
#               1       Usage error. System not modified.
#               2       User chose to not apply patch. System not modified.
#               3       Invalid patch directory. System not modified.
#               4       Multiple instances of an rpm found. System not modified.
#               5       One or more rpm installations failed. Patch is
#                       partially applied.
#               6       User id is not root
#

_version="1.0.0.2"

# Arguments passed to rpm(8) when we upgrade an already installed rpm
# We use -F (freshen) because we only want to upgrade rpm's that are
# installed on the system.
# Note: Freshen doesn't work with x86_64 packages. Use Upgrade instead.
# --replacefiles: allows to override errors when files belongs to 2 pkgs
# --replacepgks: prevents error when the package with same version is already installed.
_rpm_freshen_args="-Uhv --nodeps --replacefiles --replacepkgs"
#_rpm_freshen_args="-Fhv"

# Arguments passed to rpm(8) when installing a new rpm (only applicable
# when the "installpatch -i" is used)
_rpm_install_args="-ihv"

# File to get list of patch rpms from
_rpmlistfile="rpmlist.txt"

# Command Name
_cmdname=`basename $0`

#
# Function print_list
# Prints three lists in a somewhat formatted fashion
#
# Inputs:
#    list1
#    list2
#    list3
#
# Output:
#    Prints contents of lists like:
#
#    list1_item1 list2_item1 list3_item1
#    list1_item2 list2_item2 list3_item2
#    list1_item3 list2_item3 list3_item3
#
print_list () {
    if [ -n "$1" ]; then
        _n=1;
        for _s1 in $1; do 
            _s2=`echo $2 | cut -f $_n -d" "`
            _s3=`echo $3 | cut -f $_n -d" "`
            if [ -z "$_s2" -a -z "$_s3" ]; then
                printf "%30s\n" "$_s1"
            else 
                printf "%30s %30s %15s\n" "$_s1" "$_s2" "$_s3"
            fi
            _n=`expr $_n + 1`
        done
    fi
}

#
# Checks to see if a string from an rpm -q has more than one entry.
# If it does, then two versions of the rpm are installed on the system
# using different prefixes. We can't handle this -- or at least for
# now we choose not to!
#
check_for_multi () {

    _s=`echo $1 | cut -s -f 2 -d" "`
    if [ -n "$_s" ]; then
        echo
        echo "ERROR: the following rpm has more than one instance installed on"
        echo "the system. This utility cannot determine which instance to patch:"
        print_list "$1" "" ""
        echo "Either remove the extra instance(s) and re-run this utility, or"
        echo "Install the update rpm manually using:"
        echo "    rpm $_rpm_freshen_args --prefix <install_prefix> <rpm>"
        echo "You can get the install prefix of the installed rpm using:"
        echo "    rpm -q --queryformat '%{INSTALLPREFIX}' <rpm>"
        echo "No updates have been applied to the system"
        exit 4
    fi

}

nothing_to_install () {
    echo "No RPMs to update"
    exit 0
}

is_all_duplicates () {
    # Checks a string of space sperated items. If every item in the string
    # is identical then return 1, else 0
    _first=""
    for _s in $1; do
        if [ -z "$_first" ]; then
            _first="$_s"
        elif [ "$_s" != "$_first" ]; then
            return 0
        fi
    done

    return 1
}

# Usage
usage () {
    echo "$_cmdname [-y] [-i] [-V] [-h] [<patch>]"
    echo "     -y: Automatically answer 'y' to all questions"
    echo "     -i: Force installation of patch rpms even if no version of "
    echo "         the rpm is already installed on the system. The default"
    echo "         behavior is to only freshen previously installed rpms"
    echo "     -V: Print version then exit"
    echo "     -h: Print usage then exit"
    echo "<patch>: Path to the directory containing the patch. If ommitted"
    echo "         assumes current directory."
    exit 1
}

# Version
version () {
    echo "$_version"
    exit 0
}

#--------------------------------- MAIN ------------------------------------
_yflag=
_iflag=
_patchdir=.
while getopts yiVh _option
do
    case $_option in
    y)  _yflag=1;;
    i)  _iflag=1;;
    V)  version;;
    h)  usage;;
    \?)  usage;;
    esac
done


# See if the patch directory was specfieid
shift `expr $OPTIND - 1`
_patchdir="$*"

if [ -z "$_patchdir" ]; then
    # No patchdir specified. Using current directory
    _patchdir=.
fi

_uid=`id | cut -f2 -d= | cut -f1 -d"("`
if [ $_uid -ne 0 ]; then
    echo "You must be root to execute this command"
    exit 6
fi

if [ ! -d "$_patchdir" ]; then
    echo "Invalid patch directory: $_patchdir";
    exit 3;
fi

# The rpmlistfile contains the list of "patch" rpm files we are supposed to
# apply to the system in the order specified. If there is no rpmlistfile
# then we just grap the rpms in the current directory.
if [ -f "$_patchdir/$_rpmlistfile" ]; then
    _patch_rpms=`cat "$_patchdir/$_rpmlistfile" | grep -v "^#"`
else
    _patch_rpms=`cd $_patchdir; /bin/ls *.rpm`
fi

# Make sure we have rpms to install
if [ ! -n "$_patch_rpms" ]; then
    echo "No patch rpms to install. Exiting."
    exit 0
fi

# List of rpm patch files whose base rpm is already installed on the system
_rpm_file_list=
# Same as above, but not full file name, just rpm name and version
_rpm_list=

# List of the rpms (with version) that are installed on the system
_installed_rpm_list=

# List of prefixes for installed rpms
_rpm_prefix_list=

# List of RPMs not installed
_not_installed_rpm_list=

# List of RPMs we had an error installing
_error_rpms=

echo
printf "Scanning system"

# Traverse list of rpm files that are in patch
for  _rpm_file in $_patch_rpms; do

    printf "."
    
    # Get rpm name from the rpm file
    _rpm_name=`rpm -qp --queryformat '%{NAME}' $_patchdir/$_rpm_file`
    _rpm_name_arch=`rpm -qp --queryformat '%{NAME}.%{ARCH}' $_patchdir/$_rpm_file`
    _rpm_name_full=`rpm -qp --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' $_patchdir/$_rpm_file`

    # Check if some version of the rpm is already installed on the system.
    rpm -q --quiet $_rpm_name_arch
    _really_installed=`rpm -q $_rpm_name_arch`
    if [ $? -ne 0 -o -z "$_really_installed" ]; then
        _not_installed_rpm_list="$_not_installed_rpm_list $_rpm_file_arch"
        continue
    fi

    # Some version of the rpm is installed on the system. 
    # Get version of the rpm installed so we can tell the user
    _installed_rpm=`rpm -q $_rpm_name`
    _installed_rpm_arch=`rpm -q $_rpm_name_arch`
    _installed_rpm_full=`rpm -q --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' $_rpm_name_arch`

    # Check if multiple instance of the rpm is installed on the system.
    # If there are we will punt.
    check_for_multi "$_installed_rpm_arch";

    # Query rpm file and get default prefix
    _rpm_prefix=`rpm -qp --queryformat "%{prefixes}" $_patchdir/$_rpm_file`
    # Query installed rpm and prefix of installed package
    _installed_prefix=`rpm -q --queryformat '%{INSTALLPREFIX}' $_rpm_name_arch`


    # If there is no installed_prefix set it to "(none)".
    if [ ! -n "$_installed_prefix" ]; then
        _installed_prefix="(none)"
    fi

    if [ ! -n "$_rpm_prefix" ]; then
        _rpm_prefix="(none)"
    fi

    # Add rpm file to list of rpms to install
    _rpm_file_list="$_rpm_file_list $_rpm_file"

    # We also query the file and get the rpm name+version info. This
    # is used only when displaying the list to the user (since it is
    # a bit more compact)
    _rpm_list="$_rpm_list $_rpm_name_full"

    # Add rpm version to list of rpms already installed
    _installed_rpm_list="$_installed_rpm_list $_installed_rpm_full"

    # Save prefix as appropriate
    if [ "$_installed_prefix" = "$_rpm_prefix" -o "$_installed_prefix" = "(none)" -o "$_installed_prefix" = "none" ]; then
        # No prefix used in installed rpm, assume default install location
        _rpm_prefix_list="$_rpm_prefix_list (default)"
    else
        # Installed package was relocated. Save installed prefix for later use
        _rpm_prefix_list="$_rpm_prefix_list $_installed_prefix"
    fi
done

printf "\n\n"

# Tell the user about the rpms we plan on updating
if [ -n "$_rpm_list" ]; then
    echo "The following RPMs will be updated onto the system."
    echo "If the version of the RPM already installed on the system is"
    echo "greater than or equal to the version of the Patch RPM, then the"
    echo "version currently installed on the system will not be modified."
    echo
    printf "%30s %30s %15s\n" "Patch RPM" "Installed RPM" "Install Prefix"
        printf "%s\n" "-----------------------------------------------------------------------------"
    print_list "$_rpm_list" "$_installed_rpm_list" "$_rpm_prefix_list"
    echo
fi

# Tell the user about the rpms that we could update, but we aren't
# because they aren't on the system.
if [ -n "$_not_installed_rpm_list" -a -z "$_iflag" ]; then
    echo "The following RPMs will not be updated onto the system because no"
    echo "version of the RPMs are currently installed on the system: "
    echo
    printf "%30s\n" "Patch RPM" 
    printf "%s\n" "------------------------------"
    print_list "$_not_installed_rpm_list"  "" ""
    echo
fi

# Or tell them we will install (because -i was passed)
if [ -n "$_not_installed_rpm_list" -a -n "$_iflag" ]; then
    echo "The following RPMs will be installed onto the system even though"
    echo "no versions of the RPMs are currently installed on the system."
    echo
    printf "%30s\n" "Patch RPM" 
    printf "%s\n" "------------------------------"
    print_list "$_not_installed_rpm_list"  "" ""
    echo
fi

# If there is nothing to install then say so and exit
if [ -z "$_rpm_file_list" ]; then
    # No rpm's to update
    if [ -z "$_not_installed_rpm_list" ]; then
        # No rpm's to install
        nothing_to_install
    fi
    if [ -n "$_not_installed_rpm_list" -a -z "$_iflag" ]; then
        # Could have rpm's to install, but -i was not specified so we don't
        nothing_to_install
    fi
fi

echo "You will not be able to rollback this update."

# Ask for user confirmation
while [ "$_ans" != "y" ]; do
    printf "Continue with installation? [y|n]: "
    if [ -n "$_yflag" ]; then
        echo "y"
       _ans="y"
    else
        read _ans
    fi
    if [ "$_ans" = "n" ]; then
       exit 2
    fi
done

echo
echo "Updating system..."
echo

if [ -n "$_rpm_file_list" ]; then
    # Update rpms
    # It would be nice to do them all in one invocation of "rpm -F", but
    # some may need to be installed with --prefix. So we do one at a time.
    _n=1;
    for _rpm_file in $_rpm_file_list; do

        # Get prefix for this rpm
        _prefix=`echo $_rpm_prefix_list | cut -f $_n -d" "`

        if [ $_prefix = "(default)" ]; then
            # Package is in default location, don't use --prefix
            echo "rpm $_rpm_freshen_args $_patchdir/$_rpm_file"
            rpm $_rpm_freshen_args "$_patchdir/$_rpm_file"
        else
            # Package is not in default location, use --prefix
            echo "rpm $_rpm_freshen_args --prefix $_prefix $_patchdir/$_rpm_file"
            rpm $_rpm_freshen_args --prefix "$_prefix" "$_patchdir/$_rpm_file"
        fi

        if [ $? -ne 0 ]; then
            # If the rpm operation failed remember what rpm file it failed on
            _error_rpms="$_error_rpms $_rpm_file"
        fi

        _n=`expr $_n + 1`
    done
fi

# If the -i option was passed, then install the rpms that do not
# have a base version already installed.
if [ -n "$_not_installed_rpm_list" -a -n "$_iflag" ]; then
    # Install rpms
    for _rpm_file in $_not_installed_rpm_list; do
        echo "rpm $_rpm_install_args $_patchdir/$_rpm_file"
        rpm $_rpm_install_args "$_patchdir/$_rpm_file"
        if [ $? -ne 0 ]; then
            # If the rpm operation failed remember what rpm file it failed on
            _error_rpms="$_error_rpms $_rpm_file"
        fi
    done
fi

if [ -n "$_error_rpms" ]; then
    # Bummer, we had an error installing one or more rpms. Tell the user
    echo
    echo "An error occured installing one or more RPMs. The RPMs that"
    echo "did not install cleanly are:"
    echo
    printf "%30s\n" "Failed Patch RPM" 
    printf "%s\n" "------------------------------"
    print_list "$_error_rpms"  "" ""
    echo
    echo "You may run $_cmdname again after correcting the error."
    echo "You may also try to install the failed RPMs manually using"
    echo "    rpm $_rpm_freshen_args --prefix <install_prefix> <list_of_rpms>"
    echo "You can determine the install prefix of the installed RPM using:"
    echo "    rpm -q --queryformat '%{INSTALLPREFIX}' <rpm>"
    echo
    echo "Patch installation did not complete successfully"
    exit 5;
else
    # Yahooo! All went well
    echo
    echo "Patch installation completed successfully."
    exit 0;
    fi
