#!/bin/bash
#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE
#------------------------------------------------------------------------------

# validate_sdp.sh
# Validates current SDP environment according to current version of SDP

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

export SDP_ENV=/p4/common/bin/p4_vars
export SDP_INSTANCE=${SDP_INSTANCE:-Unset}

export SDP_INSTANCE=${1:-$SDP_INSTANCE}
if [[ $SDP_INSTANCE == Undefined ]]; then
   echo "Instance parameter not supplied."
   echo "You must supply the Perforce instance as a parameter to this script."
   exit 1
fi

declare -i ServerOnline=0
declare -i ErrorCount=0
declare -i CheckCount=0
declare -i ExitCode=0
declare -i ShowLog=1
declare Version=5.1.0
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
export LOGFILE=Unset

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

# Note: This script does not use SDP library files, as its purpose is to
# verify the integrity of an SDP installation.  Thus, it has its own
# self-contained versions of some functions that would normally be
# sourced in from files like /p4/common/lib/libcore.sh.

# Micro-functions, one-liners used to avoid external dependencies.
function msg () { if [[ "$LOGFILE" != Unset ]]; then echo -e "$*" >> "$LOGFILE"; else echo -e "$*"; fi; }
function errmsg () { msg "Error: ${1:-Unknown Error}\n"; }
function bail () { errmsg "${1:-Unknown Error}"; exit ${2:-1}; }

function run () { 
   local cmd="${1:-echo}"
   local desc="${2:-}"
   local -i showOutput="${3:-1}"
   local log=$(mktemp)
   local -i exitCode=

   [[ -n "$desc" ]] && msg "$desc"
   msg "Executing: $cmd"
   $cmd > "$log" 2>&1
   exitCode=$?

   if [[ "$showOutput" ]]; then
      echo "EXIT_CODE: $exitCode" >> $log
      cat $log >> $LOGFILE
   fi

   /bin/rm -f $log
   return $exitCode
}

#------------------------------------------------------------------------------
# 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 -man
# usage -h "Incorrect command line usage."
#
# This last example generates a usage error message followed by the short
# '-h' usage summary.
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare errorMessage=${2:-Unset}

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

   echo "USAGE for validate_sdp.sh v$Version:

validate_sdp.sh [<instance>]

   or

validate_sdp.sh -h|-man
"
   if [[ $style == -man ]]; then
      echo -e "DESCRIPTION:

	This script validates the current SDP setup for the specified instance.

	Useful if you change anything, particularly after an SDP upgrade.

OPTIONS:
<instance>
	Specify the SDP instances.  If not specified, the SDP_INSTANCE
	environment variable is used instead.  If the instance is not
	defined by a parameter and SDP_INSTANCE is not defined,
	exits immediately with an error message.

 -online
	Online mode.  Does additional checks that require P4D to be online.

 -si	Silent mode.  Does not display the generated log file to stdout
	at the end of processing.

 -L <log>
	Specify the log file to use.  The default is /p4/N/logs/validate_SDP.log

 -D     Set extreme debugging verbosity.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message

EXAMPLES:
	This script is typically called after SDP update with only the instance
	paramter as an argument, e.g.:
	validate_SDP.sh N

LOGGING:
	This script generates a log file and also displays it to stdout at the
	end of processing.  By default, the log is:
	/p4/N/logs/validate_SDP.log.

	The exception is usage errors, which result an error being sent to
	stderr followed usage info on stdout, followed by an immediate exit.

	If the '-si' (silent) flag is used, the log is generated, but its contents
	are not displayed to stdout at the end of processing.

EXIT CODES:
	An exit code of 0 indicates no errors were encounted attempting to
	perform validations, and that all checks verified cleanly.
"
   fi

   exit 1
}

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

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) usage -h;;
      (-man) usage -man;;
      (-online) ServerOnline=1;;
      (-si) ShowLog=0;;
      (-L) LOGFILE="$2"; shiftArgs=1;;
      (-D) set -x;; # Debug; use 'set -x' mode.
      (-*) usage -h "Unknown command line option ($1).";;
      (*) export SDP_INSTANCE=$1;;
   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

[[ $SDP_INSTANCE == Unset ]] && \
    bail "The \$SDP_INSTANCE setting is not defined. It must be defined by doing:\n\n\tsource $SDP_ENV <instance>\n\nor by passing in the instance name as a parameter to this script.\n"

function check_file () {
   local file=$1
   local err_msg=${2:-Missing file}
   CheckCount+=1
   msg "Checking existence of file $file"
   [[ -f $file ]] && return 1
   msg "$err_msg [$file]."
   ErrorCount+=1
   return 0
}

#------------------------------------------------------------------------------
# Check that a configurable is set, and optionally check that it is set to
# an expected value.
#
# Inputs:
# $1 - SDP Instance. Required.
# $2 - Configurable name. Required.
# $3 - Configurable scope. Default is "any"
# $4 - Expected value of variable. Optional. If defined, an additional check is
#      done, checking the current value against the expected value.
# $5 - Optional error message to display if no value is defined.  See code
#      below for the default message.
# $6 - Optional error message to display if a value is defined but does not match
#      the expected value.  See code below for the default message.
#
# Return Codes:
# 1 - Verifications failed.
# 0 - Verifications passed.
# 
# Sample Usage: 
# check_configurable $SDP_INSTANCE journalPrefix

# check_configurable $SDP_INSTANCE journalPrefix any "$CHECKPOINTS/$P4SERVER"
#
# check_configurable $SDP_INSTANCE journalPrefix any "$CHECKPOINTS/$P4SERVER" ||\
#   bail "Yikes, journalPrefix is not set, all bets are off. Aborting."
#------------------------------------------------------------------------------
function check_configurable () {
   local instance="$1"
   local configurable="$2"
   local scope="${3:-any}"
   local expectedValue="${4:-NoExpectedValue}"
   local errMsgMissing="${5:-No value defined}"
   local errMsgBadValue="${6:-Value does not match what is expected}"
   local value=
   CheckCount+=1

   value=$($P4DBIN -r $P4ROOT -cshow | grep "^${scope}: ${configurable} = ")

   if [[ -n "$value" ]]; then
      value=${value##* = }
      msg "Verified: Configurable ${scope}:${configurable} is defined."
   else
      errmsg "$errMsgMissing for configurable [${scope}:${configurable}]."
      ErrorCount+=1
      return 1
   fi

   [[ "$expectedValue" == "NoExpectedValue" ]] && return 0

   CheckCount+=1

   if [[ "$value" == "$expectedValue" ]]; then
      msg "Verified: Configurable ${scope}:${configurable} has expected value [$value]."
   else
      errmsg "$errMsgBadValue for variable [${scope}:${configurable}]\n\tExpected value: [$expectedValue]\n\tActual value:   [$value]"
      ErrorCount+=1
      return 1
   fi

   return 0
}

#------------------------------------------------------------------------------
# Check that a shell environment variable is set when sourcing the SDP
# environment. Optionally checks that variables are set to expected values.
#
# Inputs:
# $1 - SDP Instance. Required.
# $2 - Variable name. Required.
# $3 - Expected value of variable. Optional. If defined, an additional check is
#      done, checking the current value against the expected value.
# $4 - Optional error message to display if no value is defined.  See code
#      below for the default message.
# $5 - Optional error message to display if a value is defined but does not match
#      the expected value.  See code below for the default message.
# 
# Return Codes:
# 1 - Verifications failed.
# 0 - Verifications passed.
# Sample Usage: 
# check_env_var $SDP_INSTANCE P4JOURNAL "/p4/$SDP_INSTANCE/logs/journal"
#
# check_env_var $SDP_INSTANCE P4JOURNAL "/p4/$SDP_INSTANCE/logs/journal" ||\
#   bail "Yikes, P4JOURNAL is not set, all bets are off. Aborting."
#------------------------------------------------------------------------------
function check_env_var () {
   local instance="$1"
   local var="$2"
   local expectedValue="${3:-NoExpectedValue}"
   local errMsgMissing="${4:-No value defined}"
   local errMsgBadValue="${5:-Value does not match what is expected}"
   local value=
   CheckCount+=1

   eval unset ${var}
   source "$SDP_ENV" "$instance"

   set +u
   if [[ -n "$(eval echo \$${var})" ]]; then
      msg "Verified: Variable ${var} is defined."
      set -u
   else
      errmsg "$errMsgMissing for variable [$var]."
      ErrorCount+=1
      return 1
      set -u
   fi

   [[ "$expectedValue" == "NoExpectedValue" ]] && return 0

   CheckCount+=1
   value="$(eval echo \$${var})"

   if [[ "$value" == "$expectedValue" ]]; then
      msg "Verified: Variable ${var} has expected value [$value]."
   else
      errmsg "$errMsgBadValue for variable [$var]\n\tExpected value: [$expectedValue]\n\tActual value:   [$value]"
      ErrorCount+=1
      return 1
   fi

   return 0
}


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

source "$SDP_ENV" "$SDP_INSTANCE" ||\
    bail "Failed to load SDP environment for instance $SDP_INSTANCE."

source "$P4CBIN/backup_functions.sh" ||\
    bail "Failed to load backup_functions.sh."

[[ "$LOGFILE" == Unset ]] && export LOGFILE="$LOGS/validate_SDP.log"

rotate_log_file $LOGFILE ".gz"

msg "${0##*/} v$Version Starting validation at $(date +'%a %Y-%m-%d %H:%M:%S %Z')."

msg "If you have any questions about the output from this script, contact support@perforce.com"

check_env_var $SDP_INSTANCE SDP_INSTANCE
check_env_var $SDP_INSTANCE P4ROOT "/p4/$SDP_INSTANCE/root"
check_env_var $SDP_INSTANCE P4JOURNAL "/p4/$SDP_INSTANCE/logs/journal"
check_env_var $SDP_INSTANCE SDP_ADMIN_PASSWORD_FILE "$P4CCFG/.p4passwd.$P4SERVER.admin"

if [[ $? -eq 0 ]]; then
   check_file "$SDP_ADMIN_PASSWORD_FILE" "tickets file doesn't exist"
fi

check_vars
set_vars
check_dirs

# Check db files exist
file=db.counters
for dir in $P4ROOT $OFFLINE_DB; do
   check_file "$dir/$file" "Expected database file doesn't exist"
done

check_file "$OFFLINE_DB/offline_db_usable.txt" "Offline database not in a usable state."
check_file "$P4BIN" "The p4 binary (or symlink) doesn't exist"
check_file "$RC" "The p4d init script doesn't exist"
check_file "$P4TICKETS" "The P4TICKETS file doesn't exist"

check_configurable $SDP_INSTANCE journalPrefix any "$CHECKPOINTS/$P4SERVER"
check_configurable $SDP_INSTANCE server.depot.root any "$DEPOTS"

if [[ "$ServerOnline" -eq 1 ]]; then
   msg "\n${H2}\nDoing online checks."
   CheckCount+=1
   run "$P4CBIN/p4login" "Login check" 0
   if [[ $? -eq 0 ]]; then
      msg "Verified: Login OK."
   else
      errmsg "Login as P4USER $P4USER to P4PORT $P4PORT could not be verified."
      ErrorCount+=1
   fi
fi

P4="$P4BIN -p $P4PORT -u $P4USER"

# TODO:
# - links all present and correct for instance
# - db files present (and quick validation OK)
# - offline db directories named appropriately (e.g. db1/db2)
# - crontab includes at least daily_checkpoint.sh
# - checkpoints dir contains a checkpoint or two
# - service password validated for a replica
# - Add flag to check less-critical SDP configurables, and generate
#   warnings (rather than errors) if they are not set as expected.

if [[ $ErrorCount -eq 0 ]]; then
   msg "\n${H1}\nALL CLEAN: $CheckCount verifications completed OK."
else
   msg "\n${H1}\nVerifications completed, with $ErrorCount errors detected in $CheckCount checks."
   ExitCode=1
fi

[[ "$ShowLog" -eq 1 && -s "$LOGFILE" ]] && cat "$LOGFILE"
 
exit $ExitCode
