#!/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
#------------------------------------------------------------------------------
set -u

# sdp_upgrade.sh
# Upgrades the Perforce Helix Server Deployment Package (SDP).

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

export VS_SDP_P4CBIN="/p4/common/bin"
export VS_SDP_ENV="$VS_SDP_P4CBIN/p4_vars"
export SDP_INSTANCE="${SDP_INSTANCE:-UnsetSDPInstance}"
export VS_SDP_OWNER=

declare Version=1.0.0
declare ThisScript="${0##*/}"
declare ThisUser=
declare CmdArgs="$*"
declare CmdLine="$0 $CmdArgs"
declare -i ErrorCount=0
declare -i WarningCount=0
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare Log="Unset"
declare -i PreflightOnly=0
declare -i NoOp=0

# The SDPTargetMajorVersion is hard-coded by design. This is upgraded with every SDP major
# version.  The SDPTargetVersion includes the changelist number extracted from the SDP
# 'Version' file which is updated by automation during the SDP release process.
declare SDPTargetMajorVersion=2021.1
declare SDPTargetVersion=

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

# Note: This script does not use SDP library files, as its purpose is to
# upgrade the SDP installation.
function msg () { echo -e "$*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function warnmsg () { msg "\\nWarning: ${1:-Unknown Warning}\\n"; WarningCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "${2:-1}"; }

#------------------------------------------------------------------------------
# This function takes as input an SDP version string, and returns a version
# id of the form YYYY.N.CL, where YYYY is the year, N is an incrementing
# release ID with a given year, and CL is a changelist identifier. The
# YYYY.N together comprise the major version, often shortened to YY.N, e.g.
# r20.1 for the 2020.1 release.
#
# The full SDP Version string looks something like this:
# Rev. SDP/MultiArch/2019.3/26494 (2020/04/23).
#
# This function parses that full string and returns a value like: 2019.3.26494
function get_sdp_version_from_string () {
   local versionString="${1:-}"
   local version=
   version="20${versionString##*/20}"
   version="${version%% *}"
   version="${version/\//.}"

   [[ "$version" == "20" || "$version" == "200" ]] && version="Unknown"
   echo "$version"
}

#------------------------------------------------------------------------------
# Function: run ($cmd, $desc, $showOutput)
#
# Runs a command, with optional description, showing command line to execute
# and optionally also the output, and capturing and returning the exit code.
#
# Input:
# $1 - Command and arguments to execute. Defaults to 'echo'.
# $2 - Optional message to display describing what the command is doing.
# $3 - Numeric flag to show output; '1' indicates to show output, 0 to
#      suppress it.
#------------------------------------------------------------------------------
function run () {
   local cmd="${1:-echo}"
   local desc="${2:-}"
   local -i showOutput="${3:-1}"
   local -i exitCode=
   local cmdLog

   cmdLog="$(mktemp run.XXXXXXXXXXX.log)"

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

   if [[ "$showOutput" -eq 1 ]]; then
      echo "EXIT_CODE: $exitCode" >> "$cmdLog"
      cat "$cmdLog"
   fi

   /bin/rm -f "$cmdLog"
   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
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure 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 sdp_upgrade.sh v$Version:

sdp_upgrade.sh [-n] [-si] [-L <log>|off ] [-D]

   or

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

	This script upgrades Perforce Helix Server Deployment Package (SDP) from
	SDP 2020.1 to the version included in the latest SDP version, SDP
	$SDPTargetVersion.

	== Pre-Upgrade Planning ==

	This script will upgrade the SDP if the pre-upgrade starting SDP version
	is SDP 2020.1 or later, including any/all patches of SDP
	2020.1. If the current SDP version is older than 2020.1, it should first be
	upgraded to SDP 2020.1 using the SDP Legacy Upgrade Guide. See:
	https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/doc/SDP_Legacy_Upgrades.Unix.html

	When this script is used, i.e. when the SDP version is 2020.1 or newer,
	the SDP upgrade procedure does not require downtime for any running
	Perforce Helix services, such as p4d, p4broker, or p4p.

	The SDP should always be upgraded to the latest version first before
	Helix Core services such as p4d are upgraded using the SDP upgrade.sh.
	Upgrading the SDP first will ensure the version of the SDP you have
	is compatible with the latest versions of p4d/p4broker/p4p/p4, and
	will always be compatible with all supported versions of these
	Helix Core binaries.

	This script will upgrade the SDP on a single machine. If your Perforce
	Helix topology has multiple machines, the SDP can be upgraded on all
	machines in any order, as their is no cross-machine dependency on
	requiring the SDP to be the same version. Note that the order of
	upgrade of Helix Core services and binaries such as p4d in global
	topologies with replicas and edge servers does matter.

	Planning Recap:
	1. The SDP can be upgraded without downtime when this script is used,
	i.e. when the SDP version is 2020.1 or later.
	2. The SDP should be upgraded on all machines, in any order, before
	upgrading p4d and other binaries with the SDP using the upgrade.sh
	script or using the get_helix_binaries.sh script.

	== Acquiring the SDP Package ==

	This script is part of the SDP package (tarball). It can be
	run from extracted tarball directory, implying that the SDP
	tarball has already been downloaded and extracted. However,
	this individual script can also be downloaded independently
	and use to acquire the SDP tarball if outbound internet
	access over port 443 for HTTPS is available.

	== Preflight Checks ==

	Prior to upgrading, preflight checks are performed to ensure the
	upgrade can be completed successfully. If the preflight checks
	fail, the upgrade will not start.

	This script uses the existing verify_sdp.sh script in the current,
	pre-upgrade SDP as part of its preflight checks.

	== Upgrade Processing ==

	EDITME

	== Post-Upgrade Processing ==

	EDITME

OPTIONS:
 -n	No-Op.  In No-Op mode, no actions that affect data or structures are
	taken.  Instead, commands that would be run are displayed.  This
	command can also be educational, showing various steps that will occur
	during an upgrade.

 -p	Specify '-p' to halt processing after preflight checks are complete,
	and before actual processing starts. By default, processing starts
	immediately upon successful completion of preflight checks.

 -L <log>
	Specify the log file to use.  The default is /tmp/sdp_upgrade.<timestamp>.log

	The special value 'off' disables logging to a file.

 -D	Set extreme debugging verbosity.

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

FILES AND DIRECTORIES:
	Name: SDPCommon
	Path: /p4/common
	Notes: This sdp_upgrade.sh script updates files in and under this folder.

	Name: HxDepots
	Default Path: /hxdepots
	Notes: The folder containing versioned files, checkpoints, and numbered
	journals, and the SDP itself. This is commonly a mount point.

	Name: DownloadsDir
	Default Path: /hxdepots/sdp
	Notes: The symlink /p4/sdp points here.  The path /p4/sdp is consitent;
	the HxDepots can vary.

EXAMPLES:
	Example 1: Prelight check only:

	sdp_upgrade.sh -p

	Example 2: NoOp mode:

	sdp_upgrade.sh -n

LOGGING:
	This script generates a log file, /tmp/sdp_upgrade.<timestamp>.log
	by default. See the '-L' option above.

EXIT CODES:
	An exit code of 0 indicates no errors were encountered during the
	upgrade. A non-zero exit code indicates the upgrade was aborted
	or failed.
"

   fi

   exit 1
}

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

   [[ "$Log" == "off" ]] || msg "\\nLog is: $Log\\n${H1}\\n"

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

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

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) usage -h;;
      (-p) PreflightOnly=1;;
      (-n) NoOp=1;;
      (-man) usage -man;;
      (-L) Log="$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

if [[ "$Log" == "off" ]]; then
   if [[ "$NoOp" -eq 0 && "$PreflightOnly" -eq 0 ]]; then
      usage -h "Disabling logging with '-L off' is only allowed with '-p' and/or '-n' flags."
   fi
fi

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

trap terminate EXIT SIGINT SIGTERM

[[ "$Log" == "Unset" ]] && Log="/tmp/sdp_upgrade.$(date +'%Y%m%d-%H%M').log"

if [[ "$Log" != "off" ]]; then
   [[ -e "$Log" ]] && bail "Log file already exists: $Log\\nAborting.\\n"

   if ! touch "$Log"; then
      BadLog="$Log"
      Log="off"
      bail "Couldn't touch log file [$BadLog]. Aborting."
   fi

   # Redirect stdout and stderr to a log file.
   exec > >(tee "$Log")
   exec 2>&1

   msg "${H1}\\nLog is: $Log"
fi

ThisUser=$(whoami)
msg "$ThisScript v$Version Starting SDP upgrade as $ThisUser@${HOSTNAME%%.*} at $(date +'%a %Y-%m-%d %H:%M:%S %Z') with this command line:\\n$CmdLine"

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

msg "${H2}\\nDoing Prelight checks."

if [[ "$PreflightOnly" -eq 1 ]]; then
   msg "\\nExiting early after successful preflight checks due to '-p'."
   exit 0
fi

msg "${H2}\\nUpgrading SDP."

if [[ "$ErrorCount" -eq 0 && "$WarningCount" -eq 0 ]]; then
   msg "\\n${H1}\\n\\nSDP Upgrade Completed OK."
elif [[ "$ErrorCount" -eq 0 ]]; then
   msg "\\n${H1}\\n\\nSDP Upgrade Completed with $WarningCount warnings."
else
   msg "\\n${H1}\\n\\nSDP Upgrade FAILED, with $ErrorCount errors and $WarningCount warnings. Review the output above."
fi

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