#!/bin/bash
#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Helix Management System (HMS), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce_software-hms/view/main/LICENSE
#------------------------------------------------------------------------------

#==============================================================================
# Declarations and Environment

# Allow override of P4U_HOME, which is set only when testing P4U scripts.
export P4U_HOME=${P4U_HOME:-/p4/common/bin}
export P4U_LIB=${P4U_LIB:-/p4/common/lib}
export P4U_ENV=$P4U_LIB/p4u_env.sh
export P4U_LOG=off
export VERBOSITY=${VERBOSITY:-3}

# Environment isolation.  For stability and security reasons, prepend
# PATH to include dirs where known-good scripts exist.
# known/tested PATH and, by implication, executables on the PATH.
export PATH=$P4U_HOME:$PATH:~/bin:.
export P4CONFIG=${P4CONFIG:-.p4config}

[[ -r "$P4U_ENV" ]] || {
   echo -e "\\nError: Cannot load environment from: $P4U_ENV\\n\\n"
   exit 1
}

declare BASH_LIBS=$P4U_ENV
BASH_LIBS+=" $P4U_LIB/libcore.sh"
BASH_LIBS+=" $P4U_LIB/libp4u.sh"

for bash_lib in $BASH_LIBS; do
   # shellcheck disable=SC1090
   source "$bash_lib" ||\
      { echo -e "\\nFATAL: Failed to load bash lib [$bash_lib]. Aborting.\\n"; exit 1; }
done

declare Version=1.5.0
declare -i SilentMode=0
declare TicketExpiration=
declare TicketStatus=
declare SuperAccessStatus=
declare SkipInstanceList=sample
declare NewInstanceList=
declare -i DoPreflight=1
declare -i PreflightAccessOK=1
declare -i PreflightEnvOK=1
declare -i Skipped=0
declare HTCfgFile=/p4/common/config/HelixTopology.cfg
declare SDPEnvFile=/p4/common/bin/p4_vars
declare InstanceEnvFile=
declare InstanceList=
declare Program=Unset
declare ProgramPath=
declare ProgramArgs=
export P4USER=
export P4TICKETS=/p4/hms/.p4tickets
export P4TRUST=/p4/hms/.p4trust
export P4ENVIRO=/dev/null/.p4enviro
# shellcheck disable=SC1090 disable=SC2146 disable=SC2116
P4USER=$(echo "$(source "$SDPEnvFile" hms; echo "$P4USER")")
unset P4CONFIG

export VERBOSITY=3

#==============================================================================
# Local Functions

#------------------------------------------------------------------------------
# Function: terminate
function terminate
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   vvmsg "$THISSCRIPT: EXITCODE: $OverallReturnStatus"

   # Stop logging.
   [[ "${P4U_LOG}" == off ]] || stoplog

   # Don't litter.
   cleanTrash

   # With the trap removed, exit.
   exit "$OverallReturnStatus"
}

#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional).  Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1).  If displaying an
# errror, usually $1 should be -h so that the longer usage message doesn't
# obsure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare errorMessage=${2:-Unset}

   if [[ $errorMessage != Unset ]]; then
      echo -e "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
   fi

   echo "USAGE for $THISSCRIPT v$Version:

$THISSCRIPT [[-i <instance1>[,<instance2>,...]] [-sk <instance1>[,<instance2>,...]]] [-L <log>] [-si] [-v<n>] [-n] [-D] [<program> [<program arguments ...>]]

or

$THISSCRIPT [-h|-man|-V]
"
   if [[ $style == -man ]]; then
      echo -e "
DESCRIPTION:
	This script runs a preflight check, or a command you specify, against
	all Helix instances defined in the Helix Topology configuration file.

	If no arguments are supplied, verifies that all instances can be
	centrally managed with 'p4' commands from the current host.  It does
	this by verifying that a Perforce command can execute with super user
	access and has a long-term ticket for each instance.  The $P4TICKETS
	and $P4TRUST files are effectively verified with the preflight
	check.

OPTIONS:
 -i <instance1>[,<instance2>,...]
	Specify a comma-delimited list of instances to operate on.

	By default, all instances defined in the Helix Topology configuration
	file are operated on.

 -sk <instance1>[,<instance2>,...]
	Specify a comma-delimited list of instances to ignore for processing purposes.

	By default, the instance named 'sample' is skipped, the equivalent of specifying
	'-sk sample'.  Specify '-i none' to to process all instances including the 'sample'
	instance (if defined).

 -v<n>	Set verbosity 1-5 (-v1 = quiet, -v5 = highest).

 -L <log>
	Specify the path to a log file, or the special value 'off' to disable
	logging.  By default, output (stdout and stderr) are not captured.

	When -L is used, this script is self-logging.  That is, output displayed
	on the screen is simultaneously captured in the log file.  When -L is used,
	do not run this script with redirection operators like '> log' or '2>&1',
	and do not use 'tee.'

-si	Operate silently.  All output (stdout and stderr) is redirected to the log
	only; no output appears on the terminal.  This cannot be used with
	'-L off'.
      
	This is useful when running from cron, as it prevents automatic
	email from being sent by cron directly, as it does when a script called
	from cron generates any output.

 -n	No-Op.  Prints commands instead of running them.

 -D     Set extreme debugging verbosity.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message
 -V	Dispay version info for this script and its libraries.

FILES:
	Helix Topology Config file:
	$HTCfgFile

	Cental tickets file:
	$P4TICKETS

	Cental trust file:
	$P4TRUST

EXAMPLES:
	Example 1: Preflight Chcek

	With no arguments, do a quick preflight check to ensure that
	all configured instances can be managed, and that shell envionment
	files for all instances exist:

	$THISSCRIPT

	Example 2: Shell Envionment Check

	Check the envionment for instance 1:

	$THISSCRIPT -i 1 p4 set

	WARNING: This script overrides environment settings for
	P4PORT, P4TICKETS, P4TRUST, and P4ENVIRO, so settings for
	these values may not match what is defined by setting the
	SDP environment in the usual way, i.e.:

	source $SDPEnvFile N     # where N is the instance

	Example 3: Server Version Check  for all instances

	$THISSCRIPT p4 -ztag -F %serverVersion% info -s

	Example 4: Server Version Check  for all instances, with
	higher verbosity ('-v4') to display each instance as the command is run:

	$THISSCRIPT p4 -v4 -ztag -F %serverVersion% info -s

"
   fi

   exit 1
}

#==============================================================================
# Command Line Processing

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-i) InstanceList="${2//,/ }"; shiftArgs=1;;
      (-sk) SkipInstanceList="${2/,/ }"; shiftArgs=1;;
      (-h) usage -h;;
      (-man) usage -man;;
      (-V) show_versions; exit 1;;
      (-v1) export VERBOSITY=1;;
      (-v2) export VERBOSITY=2;;
      (-v3) export VERBOSITY=3;;
      (-v4) export VERBOSITY=4;;
      (-v5) export VERBOSITY=5;;
      (-L) export P4U_LOG=$2; shiftArgs=1;;
      (-si) SilentMode=1;;
      (-n) export NO_OP=1;;
      (-D) set -x;; # Debug; use 'set -x' mode.
      (-*) usage -h "Unknown arg ($1).";;
      (*)
         Program="$1"
         shift
         # shellcheck disable=SC2124
         ProgramArgs=$@
         break
      ;;
   esac

   # Shift (modify $#) the appropriate number of times.
   shift; while [[ $shiftArgs -gt 0 ]]; do
      [[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
      shiftArgs=$shiftArgs-1
      shift
   done
done
set -u

#==============================================================================
# Command Line Verification

[[ $SilentMode -eq 1 && $P4U_LOG == off ]] && \
   usage -h "Cannot use '-si' with '-L off'."

[[ "$Program" == Unset ]] || DoPreflight=0

# Determine the list of instances to process, accounting for any instances
# skipped.
[[ -n "$InstanceList" ]] ||\
   InstanceList=$(grep '^INSTANCE' "$HTCfgFile" | cut -d '|' -f 2)

if [[ $SkipInstanceList != "none" ]]; then
   NewInstanceList=
   for i in $InstanceList; do
      Skipped=0
      for s in $SkipInstanceList; do
         if [[ "$s" == "$i" ]]; then
            Skipped=1
            break
         fi
      done

      # If the instance isn't in the skip list, add it to the list of instances.
      [[ $Skipped -eq 1 ]] || NewInstanceList+=" $i"
   done
   # shellcheck disable=SC2116
   InstanceList="$(echo "$NewInstanceList")"
   vmsg "Instance List: $InstanceList (excluding $SkipInstanceList)"
fi

#==============================================================================
# Main Program

trap terminate EXIT SIGINT SIGTERM

declare -i OverallReturnStatus=0
declare -i ExitCode=0

if [[ "${P4U_LOG}" != off ]]; then
   touch "${P4U_LOG}" || bail "Couldn't touch log file [${P4U_LOG}]."

   # Redirect stdout and stderr to a log file.
   if [[ $SilentMode -eq 0 ]]; then
      exec > >(tee "${P4U_LOG}")
      exec 2>&1
   else
      exec >"${P4U_LOG}"
      exec 2>&1
   fi

   initlog
fi

if [[ "$DoPreflight" -eq 1 ]]; then
   # shellcheck disable=SC2116
   msg "${H1}\\nPreflight Check for instances:\\n$(echo "$InstanceList")\\n"

   msg "Accesss Preflight Check\\n${H2}"

   printf "%-12s %-24s %-5s %-s\\n" "Instance" "P4PORT" "Super" "Ticket Status"
   printf "%-12s %-24s %-5s %-s\\n" "------------" "------------------------" "-----" "-----------------------------------------"
   for i in $InstanceList; do
      active=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 7)
      [[ $active == 0 ]] && continue
      host=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 5)
      port=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 8)

      if [[ $port == "ssl:"* ]]; then
         export P4PORT="ssl:$host:${port#ssl:}"
      else
         export P4PORT="$host:$port"
      fi

      TicketExpiration=$(p4 -ztag -F %TicketExpiration% -p "$P4PORT" -u "$P4USER" login -s 2>/dev/null)
      if [[ -n "$TicketExpiration" && "$TicketExpiration" -gt "$((60*60*24*31))" ]]; then
         TicketStatus="OK with long-term ticket."
      elif [[ -n "$TicketExpiration" && "$TicketExpiration" -gt "60" ]]; then
         TicketStatus="OK with short-term ticket ($TicketExpiration seconds)."
      else
         TicketStatus="ERROR, ticket not available for $P4USER on $P4PORT."
         PreflightAccessOK=0
      fi

      if [[ "$TicketStatus" == "OK"* ]]; then
         ProtectCheck=$(p4 -ztag -F %Protections0% -u "$P4USER" -p "$P4PORT" protect -o 2>/dev/null)
         if [[ -n "$ProtectCheck" ]]; then
            SuperAccessStatus="OK"
         else
            SuperAccessStatus="Error"
            PreflightAccessOK=0
         fi
      else
         SuperAccessStatus="Unknown"
         PreflightAccessOK=0
      fi

      printf "%-12s %-24s %-5s %-s\\n" "$i" "$P4PORT" "$SuperAccessStatus" "$TicketStatus"

   done

   if [[ "$PreflightAccessOK" -eq 1 ]]; then
      msg "\\nVerified: Access OK. All instances can be accessed as super from this host."
   else
      errmsg "Preflight Access check failed.  See results above."
      OverallReturnStatus=1
   fi

   msg "Environment Preflight Check\\n${H2}"

   for i in $InstanceList; do
      active=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 7)
      [[ $active == 0 ]] && continue

      InstanceEnvFile=/p4/common/config/p4_${i}.vars

      if [[ ! -r "$InstanceEnvFile" ]]; then
         errmsg "Missing instance-specific environment file: $InstanceEnvFile"
         PreflightEnvOK=0
      fi
   done

   if [[ "$PreflightEnvOK" -eq 1 ]]; then
      msg "\\nVerified: Environment is OK, all instances have environment files."
   else
      errmsg "Preflight Environment check failed.  See missing files listed above."
      OverallReturnStatus=1
   fi
fi

if [[ "$Program" != Unset ]]; then
   if [[ $Program == /* || $Program == \.* ]]; then
      # Non-path depedent, absolute or relative path specified.
      ProgramPath=$Program
   else
      # Path-dependent path specified.
      ProgramPath="$(command -v "$Program")"
   fi

   [[ -z "$ProgramPath" ]] && \
      bail "The specified program [$Program] cannot be found.  Aborting.\\n"

   [[ ! -r "$ProgramPath" ]] && \
      bail "The specified program [$Program] cannot be found.  Aborting.\\n"

   [[ ! -x "$ProgramPath" ]] && \
      bail "The specified program [$Program] is not executable.  Aborting.\\n"

   for i in $InstanceList; do
      active=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 7)
      [[ $active == 0 ]] && continue

      vmsg "For instance $i calling $Program $ProgramArgs"
      InstanceEnvFile=/p4/common/config/p4_${i}.vars

      if [[ -r "$InstanceEnvFile" ]]; then
         if [[ $NO_OP -eq 0 ]]; then
            # shellcheck disable=SC1090
            source "$SDPEnvFile" "$i"

            export P4ENVIRO=/dev/null/.p4enviro
            export P4CONFIG=FileThatDoesNotExist
            export P4TICKETS=/p4/hms/.p4tickets
            export P4TRUST=/p4/hms/.p4trust

            host=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 5)
            port=$(grep "COMPONENT|$i|p4d-mc|" "$HTCfgFile"|cut -d '|' -f 8)

            if [[ $port == "ssl:"* ]]; then
               export P4PORT="ssl:$host:${port#ssl:}"
            else
               export P4PORT="$host:$port"
            fi

            $Program $ProgramArgs
            ExitCode=$?

            if [[ $ExitCode -ne 0 ]]; then
               warnmsg "Non-zero exit code returned: $ExitCode"
               OverallReturnStatus=1
            fi
         else
            msg "NO_OP: For instance $i, would run: $Program $ProgramArgs"
         fi
      else
         errmsg "Missing instance-specific environment file: $InstanceEnvFile"
         OverallReturnStatus=1
      fi
   done
fi

if [[ $OverallReturnStatus -eq 0 ]]; then
   msg "${H}\\nAll processing completed successfully.\\n"
else
   msg "${H}\\nProcessing completed, but with errors.  Scan above output carefully.\\n" 
fi

msg "That took $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\\n"

# See the terminate() function, which is really where this script exits.
exit $OverallReturnStatus