#!/bin/bash
#
# impatch
#
# Version 1.3
# 
# [DONE] * Query installed medsrv level and viewer level.
#  * Check if medsrv is running, warn appropriately.
# [DONE]  * Create a backup of existing files which would be replaced by files in the patch.
# [DONE]  * Create a list of files contained ina patch that are not currently part of the system. 
#  * Optionally dump the current database structure and data.
# [PARTLY DONE] * Install patch, optionally skipping viewer patches or installing them so that they
#    do not prompt users to download the new viewer on login.
# [DONE] *  Keep a log of when each patch was installed and in what order.
# [DONE] * Uninstall a patch using the backup and list of new files generated during patch install.
# * Install all patch files in a directory, skipping patches already in PATCHDIR (presumeably already installed)

# v 1.2 - add support for -c argument (performs chmod, for batch-applying updates)
#         remove support for downloading servicepack (moved to applyupdates.sh)
#         remove selfupdate feature (moved to applyupdates.sh)
# v 1.3 - Check for non-medsrv files
#         Support for filenames with spaces in them
#


# Script constants, argument variables, and environmental variables UPPERCASE
# other variables lowercase


. $HOME/etc/setenv.sh

#Save IFS so we can restore it later
OIFS=$IFS
TAG=impatch[$$]
LOGGER=/usr/bin/logger
BACKUP_PREFIX=revert
ADDED_FILES=tmp/added_files
# List of files contained in a patch that do not currently exist on the system.
PATCHDIR=patch


function usage() {
    echo
    echo "Usage:"
    echo "    $0 [ -d -n -c <pre|post|both> ] -p <patchfilename> | -r <revertfilename>"
    echo "    -c [pre|post]	Perform pre and/or post chmod operation on component directory" 
    echo "    -d			Print debugging info to the screen, don't log to medsrv log"
    echo "    -n		        Don't save revert information"
    echo "    -p <patchfilename>	Install patch <patchfilename>"
    echo "    -q			Query installed patches and current viewer version"
    echo 
    echo "    NOTE: -r and -p are mutually exclusive"
    echo
    exit 1
} # End of usage

#------ Some logging functions
#---------------------------------------------
# log_error_ 
# 
# Log errors to stdout and optionally the medsrv error log
# parameter: String to log
# Requires: $LOGGER, $LOG_FACILITY, $TAG
# If $DEBUG is not null, only log to stdout
#
#---------------------------------------------
log_error_() {
    if [ -z "$DEBUG" ] ; then
        $LOGGER -p $LOG_FACILITY.err -t $TAG "$1"
        echo $1
    else
        echo $1
    fi
}

#---------------------------------------------
# log_info_ 
# 
# Log to stdout and optionally the medsrv info log
# parameter: String to log
# Requires: $LOGGER, $LOG_FACILITY, $TAG
# If $DEBUG is not null, only log to stdout
#
#---------------------------------------------
log_info_() {
    if [ -z "$DEBUG" ] ; then
        $LOGGER -p $LOG_FACILITY.info -t $TAG "$1"
        echo $1
    else
        echo $1
    fi
}


#---------------------------------------------
# logDebug 
# 
# Log gobs of debugging info to the screen
# parameter: String to log
#
#---------------------------------------------
function logDebug() {
    if [ -n "$DEBUG" ]; then
        echo $1
    fi
}

# ------ End logging functions




#---------------------------------------------
# checkreturn 
# 
# Check the return value of command,
# log appropriately, and optionally exit on failure
#
# parameters: command <exitonfailure|noexitonfailure> <reportsuccess|noreportsuccess|logsuccess>
#             command      : command to be executed
#             exitonfailure: if exitonfailure, exit on failure
#             report       : if noreportsuccess, do not report success
#                          : if reportsuccess , report success on stdout
#                          : if logsuccess, log success to system log using log_info_
#             Always logs failures using log_error_
#---------------------------------------------
function checkreturn() {
    local command="$1"
    local exitonfailure=$2
    local report=$3
    logDebug "$command"
    $command
    rval=$?
    logDebug "$command returned: $rval"
    if [ "$rval" != 0 ]; then
        case $exitonfailure in
            exitonfailure)
                log_error_ "$(basename $0): failed $command. Aborting"
                IFS=$OIFS
                exit 1
            ;;
            *)
                log_error_ "$(basename $0): failed $command."
                return 1
            ;;
        esac
    else
        case $report in
            noreportsuccess) : ;;
            reportsuccess)
                echo "$command: completed"
            ;;
            logsuccess)
                log_info_ "$(basename $0):$command: completed"
            ;;
            *) : ;;
        esac
    fi
    return 0
} # End checkreturn  





#---------------------------------------------
# do_backup 
# 
# Backs up files in a patch file so that the 
# patch can be removed later.  Keeps track of
# non-preexisting files so that those can be 
# removed. 
# 
# parameter: path/and/filename/of/patchfile
#            relative to medsrv home directory 
#
#---------------------------------------------
function do_backup() {
    patchfile=$1
    backupdir=$BACKUP_VAR
    backupfile=$BACKUP_PREFIX.$(basename $patchfile)
    filelist=$(tar -tzf $patchfile)
    echo "Creating revert file..."
    if [ -n "$DEBUG" ]; then
        echo "List of files in patch:"
        echo "$filelist"
        echo "=====> End of list"
    fi
    # Parse through $filelist to find files not already existing on the system
    # List them in a file to be included in the backup tarball so that they can be removed during a revert
    for file in $filelist; do
        # Does it exist? If it does add it to the backup list unless it's an existing directory
        if [ -e "$file" ]; then
            if [ ! -d "$file" ]; then
                tarlist=$(echo $tarlist $file) # $tarlist is the files to tar up.
            fi
        else
            newlist=$(echo $newlist $file) # newlist is files in the patch that don't currently exist on the system
        fi
    done
    if [ -n "$newlist" ]; then
        tarlist=$(echo $tarlist $ADDED_FILES)
        echo $newlist > $ADDED_FILES
        logDebug "ADDED_FILES contains: $(cat $ADDED_FILES)"
    fi
    logDebug "List of files passed to tar: $tarlist"
    checkreturn "tar -czf $backupdir/$backupfile $tarlist" exitonfailure logsuccess
} # End fo do_backup




#---------------------------------------------
# do_permissions 
# 
# Performs pre and post patch/revert permissions
# setting.
#
# parameters: <pre | post> patchfilename 
#             post - chmod each file in component that isn't a directory and isn't a pid file to be non-writeable
#             pre  - recursively chmod the topmost directories in the patch writeable (only once per directory)
#             patchfilename - Do I really need to explain this one?
#
#---------------------------------------------
function do_permissions() {
    local command=$1
    local patchfile=$2
    if [ "$1" == "pre" ]; then
        echo "Preparing file permissions for patch"
        #filelist=$(tar -tzf $patchfile)
        #for file in $filelist;  do
        #    [ -f "$(basename $file)" ] && checkreturn "chmod +w $file" exitonfailure noreportsuccess
        #    topdir=$(echo $file|sed 's/\/.*//') # Strip everything after and including the first "/"
        #    # If the it is a directory and we haven't already chmodded it, recursively chmod it writeable
        #    if [ -d "$topdir" ] && \
        #       [ "$topdir" != "tmp" ] && \
        #       [ "$topdir" != "var" ] && \
        #       ! expr match $dirlist $topdir > /dev/null 2>&1 ; then
        #           
        #           dirlist=$(echo $dirlist $topdir)
        #           checkreturn "chmod -R +w $topdir" exitonfailure noreportsuccess
        #    fi
        #done
        # check for files in component owned by root
        nonMedsrvFiles=$(find component ! -uid 530 -o ! -gid 530)
        if [ -n "$nonMedsrvFiles" ]; then
            echo;echo;echo
            echo "Found the following files in component not owned by medsrv."
            echo "these either need to be removed or ownership changed (as root) to medsrv.medsrv"
            echo "List of files:"
            echo "$nonMedsrvFiles"
            echo;echo;echo
            exit 1
        fi
        checkreturn "chmod -R +w component" exitonfailure noreportsuccess
    else
        echo "Resetting file permissions"

# Need below to deal with filenames with spaces

        IFS="
"

        for file in $(find component/ -type f); do
            if ! echo $file | grep .pid > /dev/null 2>&1 ; then
#                checkreturn "chmod a-w $file" exitonfailure noreportsuccess
                chmod a-w $file
                if [ $? -ne 0 ]; then
                    echo "Error chmod'ing file"
                fi
            fi
        done
        IFS=$OIFS
        echo "Resetting file permissions complete"
    fi
} # End of do_permissions



#---------------------------------------------
# do_patch 
# 
# Apply or revert a patch, logging to medsrv log
#
#
# parameters: <command> <patchfle>
#             command P : perform patch
#             command R : perform revert
#             patchfile : name of patch or revert file 
#
#---------------------------------------------
function do_patch() {
    # $1 command (P = patch, R = revert)
    # $2 file name to operate on 
    local command=$1
    local patchfile=$2
    patchname=`echo $patchfile | sed 's/.tar.gz//g'`
    patchreadme=README.$patchname
    [ -f "$ADDED_FILES" ] && rm -f $ADDED_FILES
    checkreturn "tar -zxf $patchfile" exitonfailure logsuccess
    case "$command" in

        P) # Command is P - We are applying a patch
			if [ -e "remove.lst" ]; then # list of files which should be removed
				# first copy them to the backup
				backupdir=$BACKUP_VAR
				backupfile=$BACKUP_PREFIX.$(basename $patchname)

				# because tar can't append to tar.gzs
				gunzip $backupdir/$backupfile.tar.gz
				tar rf $backupdir/$backupfile.tar -T remove.lst
				gzip $backupdir/$backupfile.tar

				# secondly, delete the files
				while read file; do
					if [ -f $file ]; then
						rm $file
					fi
					if [ -d $file ]; then
						rm -r $file
					fi
				done < remove.lst

				# thirdly, remove the rm file
				rm remove.lst
			fi

            [ ! -d "$PATCHDIR" ] && mkdir $PATCHDIR
            [ -z "$DEBUG" ] && mv $patchfile $PATCHDIR
            [ -f "$patchreadme" ] && mv $patchreadme $PATCHDIR
            log_info_ "$(basename $0): Successfully applied $patchfile."
        ;;

        R) # Command is R - We're reverting
            if [ -e "$ADDED_FILES" ]; then 
            # $ADDED_FILES is the list of files that were new in the patch
                echo "$(basename $0): Removing files."
                for added_file_name in $(cat $ADDED_FILES);  do
                    if [ -d "$added_file_name" ]; then
                    # It's a directory.  Save it for later. 
                    # Only delete directories if they are empty after reverting
                        added_directories=$(echo $added_directories $added_file_name)
                    else     
                        rm -f $added_file_name
                        log_info_ "$(basename $0): removed file $added_file_name during revert"
                    fi
                done
                if [ -n "$added_directories" ]; then
                    echo "Checking for directories to be removed"
                    for added_directory_name in $(echo $added_directories);  do
                        rmdir $added_directory_name
                        result=$?
                        if [ "$result" != 0 ]; then
                            logDebug "$(basename $0): directory $added_directory_name not empty - not removed during revert"
                        else
                            log_info_ "$(basename $0): removed empty directory $added_directory_name during revert"
                        fi
                    done
                fi
            else
                logDebug "No files to remove during revert"
            fi
            log_info_ "$(basename $0): Successfully reverted using $patchfile."
            [ -z "$DEBUG" ] && mv $patchfile $TMP
        ;;
    esac
} # End of do_patch


#---------------------------------------------
# do_query_version
# 
# queries last installed patch and current
# installed viewer version
# 
# parameters : none
#
#---------------------------------------------
do_query_version() {
    echo 
    echo "Software version information for $(hostname -i) ($(hostname))"
    echo
    echo "*******************************************************************"
    echo -e "Server version:\t\t$(head -1 $HOME/etc/component.ls|sed 's/from helen.eradimagemedical.com, Version //')"
    echo -e "Viewer version:\t\t$(cat $HOME/component/download/version.txt)"
    echo
    if [ ! -f "$HOME/etc/sp_id" ]; then
        echo "Currently installed patches:"
        echo "$(ls -1tr $HOME/patch)"
    else
        echo -e "Service pack:\t\t$(cat $HOME/etc/sp_id)" 
    fi
    echo "*******************************************************************"
    echo
    exit 0
} # End of do_query_version
 

#---------------------------------------------
# parse_args 
# 
# parses arguments and sets options to the script
# 
# parameters : all arguments to the script ("$@")
#
#---------------------------------------------
function parse_args() {
    if [ -n "$1" ]; then
        while [ -n "$1" ]; do
            case $1 in
                -c)
                    case "$2" in
                        "")
                            echo
                            echo "Missing parameter <pre|post>"
                            usage
                        ;;

                        pre)
                            CHMOD_MODE=pre
                            shift
                        ;;

                        post)
                            CHMOD_MODE=post
                            shift
                        ;;

                        both)
                            CHMOD_MODE=both
                            shift
                        ;;

                        *)
                            echo
                            echo "-c requires either 'pre', 'post', or 'both' as a secondary parameter"
                            usage
                        ;;
                    esac
                ;;

                -d)
                    DEBUG=1
                ;;

                -n)
                    NOREVERT=1
                ;;
    

                -p)
                    if [ -z "$COMMAND" ]; then
                        case "$2" in
                            "")
                                echo
                                echo "No patch file specified. Aborting."
                                usage
                            ;;
                            *)
                                if [ -f "$2" ]; then
                                    PATCHFILE=$2
                                    shift
                                else
                                    echo
                                    echo "That patch file does not exist! Aborting."
                                usage
                                fi
                            ;;
                        esac
                        COMMAND=P
                    else
                        echo
                        if [ "$COMMAND" == "R" ]; then
                            echo "-p and -r are mutually exclusive options.  Aborting."
                        else
                            echo "Multiple -p parameters specified. Aborting"
                        fi
                        usage
                    fi
                ;;


                -q)
                    QUERYVERSION=1
                ;;
 
    
                -r)
                    if [ -z "$COMMAND" ]; then
                        case "$2" in
                            "")
                                echo
                                echo "No revert file specified. Aborting."
                                usage
                            ;;
                            *)
                                if [ -f "$2" ]; then
                                    PATCHFILE=$2
                                    shift
                                else
                                    echo
                                    echo "That revert file does not exist! Aborting."
                                    usage
                                fi
                            ;;
                        esac
                        COMMAND=R
                    else
                        echo
                        if [ "$COMMAND" == "P" ]; then
                            echo "-p and -r are mutually exclusive options.  Aborting."
                        else
                            echo "Multiple -r parameters specified. Aborting"
                        fi
                        usage
                    fi
                ;;


                *)
                    echo
                    echo "Unknown argument: $1"
                    echo "Aborting."
                    usage
                ;;
    
            esac
            shift
        done
        # If no patchfile arg, complain and exit
        if [ -z "$PATCHFILE" ] && [ -z "$QUERYVERSION" ]; then
            echo
            echo "No patch file or revert file specified. Aborting."
            usage
        fi
    else
        usage
    fi
} #end of parse_args


# Main 
parse_args $@

if [ -n "$QUERYVERSION" ]; then
   do_query_version
fi

if [ "$COMMAND" == "P" ]; then
    if [ -n "$NOREVERT" ]; then
        log_info_ "$(basename $0): invoked with -n option - not creating a revert file."
    else 
        do_backup $PATCHFILE
    fi
fi

if [ "$CHMOD_MODE" == "pre" -o "$CHMOD_MODE" == "both" ]; then
    do_permissions pre $PATCHFILE
fi

do_patch $COMMAND $PATCHFILE

if [ "$CHMOD_MODE" == "post" -o "$CHMOD_MODE" == "both" ]; then
    do_permissions post
fi

exit 0
