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

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

# Version ID Block. Relies on +k filetype modifier.
#------------------------------------------------------------------------------
# shellcheck disable=SC2016
declare VersionID='$Id: //p4-sdp/dev_c2s/Server/Unix/setup/mkdirs.sh#3 $ $Change: 31580 $'
declare VersionStream=${VersionID#*//}; VersionStream=${VersionStream#*/}; VersionStream=${VersionStream%%/*};
declare VersionCL=${VersionID##*: }; VersionCL=${VersionCL%% *}
declare Version=${VersionStream}.${VersionCL}
[[ "$VersionStream" == r* ]] || Version="${Version^^}"

declare ThisScript="${0##*/}"
declare RunDir="${0%%/*}"
declare CmdLine="$ThisScript $*"
declare ThisUser=
declare ThisHost=${HOSTNAME%%.*}
declare SDPInstance=UnsetSDPInstance
declare SDPInstallRoot="/p4"
declare PerforcePackageBase="/opt/perforce"
declare SDPPackageBase="$PerforcePackageBase/helix-sdp"
declare SDPPackageBins="$SDPPackageBase/helix_binaries"
declare ImmutableSDPDir="$SDPPackageBase/sdp"
declare WritableP4Dir="$SDPPackageBase/p4"
declare WritableSDPDir="$WritableP4Dir/sdp"
declare SDPMountPointBase=""
declare SymlinkTarget=
declare ConfigFile=
declare SetcapCmd=
declare TmpFile=
declare Cmd=
declare GenSudoersScript=
declare FirewallType=
declare FirewallDir=
declare -i LoadActiveCrontab=1
declare -i CrontabFileGenerated=0
declare -i DoFirewall=1
declare -i FirewallOnline=0
declare -i NoOp=0
declare -i Debug=0
declare -i PreflightOnly=0
declare -i TestMode=0
declare -i TestCleanup=0
declare -i DoChownCommands=1
declare -i DoNFSChownCommands=1
declare -i UpdateSudoers=0
declare -i LimitedSudoers=1
declare -i EnableSystemdServices=1
declare -i RunningAsRoot=0
declare -i InstallP4D=1
declare -i InstallP4Broker=1
declare -i InstallP4Proxy=0
declare -i InstallP4DTG=0
declare -i ErrorCount=0
declare -i WarningCount=0
declare -i UsePackageDirs=0
declare -i LimitedSudoers=2
declare SELinuxMode=
declare BinList=
declare InitMechanism=
declare InitScripts=
declare SDPVersionString=
declare SDPVersionFile=
declare SDPVersion=
declare OverrideCD=
declare OverrideDD=
declare OverrideLG=
declare OverrideDB1=
declare OverrideDB2=
declare SystemdTemplatesDir=
declare TarRoot=
declare TarRootCommon=
declare TarRootConfig=
declare TarRootCron=
declare TarRootBinDir=
declare TarRootInit=
declare TarRootSSL=
declare TarRootUnsupported=
declare ServerID=
declare TargetServerID=
declare TargetPort=
declare ListenPort=
declare ServerType=
declare -i ServerIDSet=0
declare -i TargetPortSet=0
declare -i ListenPortSet=0
declare -i FastMode=0
declare -i UseSystemd=1
declare -i DoServiceInit=1
declare Log=
declare ExtendPermsTo=
declare ExecPerms=
declare ReadPerms=
declare CrontabFile=
declare CrontabFileInP4=
declare SDPCrontabDir=
declare P4BinRel=
declare InheritedUmask=
declare OperatingUmask="0022"

#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
function dbg () { [[ "$Debug" -eq 0 ]] || msg "DEBUG: $*"; }
function errmsg () { msg "\\nError: (line: ${BASH_LINENO[0]}) $*" >&2; ErrorCount+=1; }
function warnmsg () { msg "\\nWarning: (line: ${BASH_LINENO[0]}) $*"; WarningCount+=1; }
function bail() { errmsg "(line: ${BASH_LINENO[0]}) ${1:-Unknown Error}\\n" >&2; 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/\//.}"
   echo "$version"
}

#------------------------------------------------------------------------------
# Function: run ($cmd, $desc, $honorNoOp)
#
# Input:
#
# $cmd  - Command to execute, including arguments.?
#
# $desc - Description of command to run. Optional; default is no description.
#
# $honorNoOp - If set to 1, command is always executed; NoOp setting is ignored.
#    Optional, default is 0.
#------------------------------------------------------------------------------
function run () { 
   declare cmd="${1:-echo}"
   declare desc="${2:-}"
   declare honorNoOp="${3:-1}"

   [[ -n "$desc" ]] && msg "$desc"
   msg "Running: $cmd"

   if [[ "$NoOp" -eq 1 && "$honorNoOp" -eq 1 ]]; then
      msg "NO_OP: Would run $cmd"
      return 0
   else
      # shellcheck disable=SC2086
      if eval $cmd; then
         return 0
      else
         return 1
      fi
   fi
}

#------------------------------------------------------------------------------
# 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 -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare usageErrorMessage=${2:-Unset}

   if [[ "$usageErrorMessage" != Unset ]]; then
      msg "\\n\\nUsage Error:\\n\\n$usageErrorMessage\\n\\n"
   fi

   # tag::includeManual[]
   msg "USAGE for $ThisScript version $Version:

$ThisScript <instance> [-r <P4BinRel>] [-s <ServerID>] [-t <ServerType>] [-tp <TargetPort>] [-lp <ListenPort>] [-I <svc>[,<svc2>]] [-MDD /bigdisk] [-MCD /ckps] [-MLG /jnl] [-MDB1 /db1] [-MDB2 /db2] [-f] [-p] [-no_init|-no_systemd|-no_enable] [-fs|-ls] [-no_cron] [-no_firewall] [-test [-clean]] [-n] [-L <log>] [-d|-D]

OR

$ThisScript <instance> [-c <CfgFile>] [-f] [-p] [-no_init|-no_systemd|-no_enable] [-fs|-ls] [-no_cron] [-no_firewall] [-test [-clean]] [-n] [-L <log>] [-d|-D]

or

$ThisScript [-h|-man]
"
   if [[ $style == -man ]]; then
      msg "
DESCRIPTION:

== Overview ==

This script initializes an SDP instance on a single machine.

This script is intended to support two scenarios:

* First time SDP installation on a given machine.  In this case, the
  user calls the install_sdp.sh script, which in turn calls this script.
  See 'install_sdp.sh -man' for more information.

* Adding new SDP instances (separate Helix Core data sets) to an existing
  SDP installation on a given machine. For this scenario, this mkdirs.sh
  script is called directly.

An SDP instance is a single Helix Core data set, with its own unique
set of one set of users, changelist numbers, jobs, labels, versioned
files, etc. An organization may run a single instance or multiple
instances.

This is intended to be run either as root or as the operating system
user account (OSUSER) that p4d is configured to run as, typically
'perforce'.  It should be run as root for the initial install.
Subsequent additions of new instances do not require root.

== Directory Structure ==

If an initial install as done by a user other than root, various
directories must exist and be writable and owned by 'perforce' before starting:

* /p4
* /hxcheckpoints
* /hxdepots
* /hxlogs
* /hxmetadata
* /hxmetadata2
* /opt/perforce/helix-sdp (optional; used for package installations)

The directories starting with '/hx' are configurable, and can be changed by
settings in the mkdirs.cfg file (or mkdirs.N.cfg), or with command line
options as illustrated here:

 -MDD /bigdisk
 -MCD /ckps
 -MLG /jnl
 -MDB1 /db1
 -MDB2 /db2

This script creates an init script in the /p4/N/bin directory.

== Crontab ==

Crontabs are generated for all server types.

After running this script, set up the crontab based on templates
generated as /p4/common/etc/cron.d.  For convenience, a sample crontab
is generated for the current machine as /p4/p4.crontab.<SDPInstance> 
(or /p4/p4.crontab.<SDPInstance>.new if the former name exists).

These files should be copied or merged into any existing files named
with this convention:

/p4/common/etc/cron.d/crontab.<osuser>.<host>

where <osuser> is the user that services run as (typically 'perforce'),
and <host> is the short hostname (as returned by a 'hostname -s' command).

== Init Mechanism ==

If this script is run as root, the init mechanism (Systemd or SysV) is
configured for installed services.

The Systemd mechanim is used if the the /etc/systemd/system folder exists and
systemctl is in the PATH of the root user. Otherwise, the SysV init mechanism
is used.

== Firewall Configuration ==

This script checks to see if a known firewall type is available.  The
firewalld is checked using the command 'firewall-cmd --state' command,
and the ufw firewall is checked using the 'ufw status'.  If either firewall
is detected, the ports required for Helix Core applications installed are
opened in the firewall.  For more information, see the templates in these folders:

/p4/common/etc/firewalld
/p4/common/etc/ufw

If the firewall service is not online, no firewall coniguration is performed.

== SELinux Configuration ==

If Systemd is used and the semanage and restorecon utilities are available in
the PATH of the root user, then SELinux configuration for the installed
services is done.

REQUIRED PARAMETERS:
 <instance>
	Specify the SDP instance name to add.  This is a reference to the Perforce
	Helix Core data set.

OPTIONS:
 -s <ServerID>
	Specify the ServerID, overriding the REPLICA_ID setting in the configuration
	file.

 -S <TargetServerID>
	Specify the ServerID of the P4TARGET of the server being installed.
	Use this only when setting up an HA replica of an edge server.

 -t <ServerType>
	Specify the server type, overriding the SERVER_TYPE setting in the config
	file.  Valid values are:
	* p4d_commit - A master/commit server.
	* p4d_master - A synonym for p4d_commit.
	* p4d_replica - A replica with all metadata from the master (not
	  filtered in any way).
	* p4d_filtered_replica - A filtered replica or filtered forwarding
	  replica.
	* p4d_edge - An edge server.
	* p4d_edge_replica - Replica of an edge server. If used,
	  '-S <TargetServerID>' is required.
	* p4broker - An SDP host running only a standalone p4broker, with no p4d.
	* p4p - An SDP host running only a standalone p4p, with no p4d.
	* p4proxy - A synonym for p4p.

 -tp <TargetPort>
	Specify the target port. Use only if ServerType is p4p and p4broker.

 -lp <ListenPort>
	Specify the listen port. Use only if ServerType is p4p and p4broker.

 -I [<svc>[,<svc2>]]
	Specify additional init scripts to be added to /p4/<instance>/bin
	for the instance.

	By default, the p4p service is installed only if '-t p4proxy' is
	specified.  p4dtg is never installed by default.  Valid values
	to specify are 'p4p' and 'dtg' (for the P4DTG init script).

	If services are not installed by default, they can be added later
	using templates in /p4/common/etc/init.d. Also, templates for
	systemd service files that call the init scripts are supplied in
	/p4/common/etc/systemd/system.

 -MDD /bigdisk
 -MCD /ckps
 -MLG /jnl
 -MDB1 /db1
 -MDB2 /db2
	Specify the '-M*' optons to specify mount points, overriding
	DD/CD/LG/DB1/DB2 settings in the config file.  Sample:

	-MDD /bigdisk -MLG /jnl -MDB1 /fast

	If -MDB2 is not specified, it is set the the same value as -MDB1 if
	that is set, or else it defaults to the same default value as DB1.

 -c <CfgFile>
	Specify the path to the configuration file to use, overriding the
	default logic of finding the file based on naming convention.

 -f	Specify -f 'fast mode' to skip chown/chmod commands on depot files.
	This should only be used when you are certain the ownership and
	permissions are correct, and if you have large amounts of existing
	data for which the chown/chmod of the directory tree would be
	time-consuming and unnecessary.

 -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.

 -no_init
	Specify '-no_init' to avoid any service configuration, which
	is done by default if running as root (using systemd if available,
	otherwise SysV).  If '-no_init' is used, then neither systemd nor
	SysV init mechanism is configured for installed services.

	This option is implied if not running as root.

	This option is implied if '-test' is used.

 -no_systemd
	Specify '-no_systemd' to avoid using systemd, even if it
	appears to be available. By default, systemd is used if it
	appears to be available.

	This is helpful in operating in containerized test environments
	where systemd does not work even if it appears to be available.

	This option is implied if the systemctl command is not available
	in the PATH of the root user.

	This option is implied if '-no_init' is used.

 -no_enable
	Specify '-no_enable' to avoid enabling systemd services to start
	automatically after a reboot. If this option is used, systemd
	services will still be created, allowing services to be manually
	started and stopped.

	Specifically, this options means the 'systemctl enable' command
	is not run for generated services.

 -no_cron
 	Specify '-no_cron' to avoid loading the crontab.

	A crontab file is generated in the $SDPInstallRoot directory, but
	but with '-no_cron, this file is not loaded as the active crontab.

 -no_firewall
 	Specify '-no_firewall' to avoid attempting firewall configuration.

	By default, if the firewalld service is found to be running,
	it is configured so that the ports for p4d and p4broker are
	open.

 -fs	Specify '-full' when calling gen_sudoers.sh to install a new, full
	sudoers file. This option is only available if running as root.

	This option is mutually exclusive with '-ls'.

	See 'gen_sudoers.sh -man' for more info.

 -ls	Specify '-limited' when calling gen_sudoers.sh to install a new,
	limited sudoers file. This option is only available if running as
	root.

	This option is mutually exclusive with '-fs'.

	See 'gen_sudoers.sh -man' for more info.

 -L <log>
	Specify the path to a log file, or the special value 'off' to disable
	logging.  By default, all output (stdout and stderr) goes to this file
	in the current directory:

	mkdirs.<instance>.<datestamp>.log

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

DEBUGGING OPTIONS:
 -test
 	Specify '-test' to execute a simulated install to /tmp/p4 as the install
	root (rather than /p4), and with the mount point directories specified in
	the configuration file prefixed with /tmp/hxmounts, defaulting to:
	* /tmp/hxmounts/hxdepots
	* /tmp/hxmounts/hxlogs
	* /tmp/hxmounts/hxmetadata

	This option implies '-no_init'.
 
 -clean
	Specify '-clean' with '-test' to clean up from prior test installs,
	which will result in removal of files/folders installed under /tmp/hxmounts
	and /tmp/p4.

 	Do not specify '-clean' if you want to test a series of installs.

 -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 is
	an alternative to -test. Unlike '-p' which stops after the preflight
	checks, with '-n' more processing logic can be exercised, with greater
	detail about what commands that would be executed without '-n'.

 -d     Increase verbosity for debugging.

 -D     Set extreme debugging verbosity, using bash '-x' mode. Also implies -d.

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

FILES:
	The mkdirs.sh script uses a configuration file for many settings.  A
	sample file, mkdirs.cfg, is included with the SDP.  After determining
	your SDP instance name (e.g. '1' or 'abc'), create a configuration
	file for it named mkdirs.<N>.cfg, replacing 'N' with your instance.

	Running 'mkdirs.sh N' will load configuration settings from mkdirs.N.cfg.

UPGRADING SDP:
	This script can be useful in testing and upgrading to new versions of
	the SDP, when the '-test' flag is used.

EXAMPLES:
	Example 1: Setup of first instance

	Setup of the first instance on a machine using the default instance name,
	'1', executed after using sudo to become root:

	$ sudo su -
	$ cd /hxdepots/sdp/Server/Unix/setup
	$ vi mkdirs.cfg

	Adjust settings as desired, e.g P4PORT, P4BROKERPORT, etc.

	$ ./mkdirs.sh 1

	A log will be generated, mkdirs.1.<timestamp>.log

	Example 2: Setup of additional instance named 'abc'.

	Setup a second instance on the machine, which will be a separate Helix
	Core instance with its own P4ROOT, its own set of users and
	changelists, and its own license file (copied from the master instance).

	Note that while the first run of mkdirs.sh on a given machine should be
	done as root, but subsequent instance additions can be done as the
	'perforce' user (or whatever operating system user accounts Perforce
	Helix services run as).

	$ sudo su - perforce
	$ cd /hxdepots/sdp/Server/Unix/setup
	$ cp mkdirs.cfg mkdirs.abc.cfg
	$ chmod +w mkdirs.abc.cfg
	$ vi mkdirs.abc.cfg

	Adjust settings in mkdirs.abc.cfg as desired, e.g P4PORT, P4BROKERPORT, etc.

	$ ./mkdirs.sh abc

	A log will be generated, mkdirs.abc.<timestamp>.log

	Example 3: Setup of additional instance named 'alpha' to run a standalone p4p
	targeting commit.example.com:1666 and listening locally on port 1666.

	$ sudo su -
	$ cd /hxdepots/sdp/Server/Unix/setup
	$ ./mkdirs.sh alpha -t p4p -tp commit.example.com:1666 -lp 1666

	Example 4: Setup of instance named '1' to run a standalone p4broker
	targeting commit.example.com:1666 and listening locally on port 1666.

	$ sudo su -
	$ cd /hxdepots/sdp/Server/Unix/setup
	$ ./mkdirs.sh 1 -t p4broker -tp commit.example.com:1666 -lp 1666

	Example 5: Setup 2 instances A and B with limited sudoers on a fresh new machine:

	$ sudo su -
	$ cd /hxdepots/sdp/Server/Unix/setup
	$ cp mkdirs.cfg mkdirs.A.cfg

	Adjust settings in mkdirs.A.cfg as desired, e.g P4PORT, P4BROKERPORT, etc.

	$ cp mkdirs.A.cfg mkdirs.B.cfg

	Adjust settings in mkdirs.B.cfg as desired, e.g P4PORT, P4BROKERPORT, etc.
	Ensure port numbers do not conflict. Then generate Instance A:

	$ ./mkdirs.sh A -ls

	A log will be generated, mkdirs.A.<timestamp>.log

	Next generate instnace B, updating the limited sudoers to reference both
	instances.

	$ ./mkdirs.sh B -ls

SEE ALSO:
	See 'install_sdp.sh -man' for more info on installing on a new machine.

	See 'gen_sudoers.sh -man' for more info on generating/replacing sudoers.

	See template:
	* systemd service file templates: /p4/common/etc/systemd/system
	* firewalld templates: /p4/common/etc/firewalld
	* ufw firewall templates: /p4/common/etc/ufw
	* Init script templates: /p4/common/etc/init.d
"
   fi

   exit 1
}

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

   msg "\\n$ThisScript: EXITCODE: $ErrorCount\\n"
   [[ "$Log" != "off" ]] && msg "Log is: $Log"

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

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

[[ $(id -u) -eq 0 ]] && RunningAsRoot=1

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-c) ConfigFile="$2"; shiftArgs=1;;
      (-r) P4BinRel="$2"; shiftArgs=1;;
      (-s) ServerID="$2"; ServerIDSet=1; shiftArgs=1;;
      (-S) TargetServerID="$2"; shiftArgs=1;;
      (-t)
         case "$2" in
            (p4d_commit|p4d_master|p4d_replica|p4d_filtered_replica|p4d_edge|p4d_edge_replica)
               ServerType="$2"
               InstallP4D=1
               InstallP4Broker=1
               InstallP4Proxy=0
            ;;
            (p4broker|p4b) ServerType="$2"; InstallP4D=0; InstallP4Broker=1; InstallP4Proxy=0;;
            (p4p|p4proxy) ServerType="$2"; InstallP4D=0; InstallP4Broker=0; InstallP4Proxy=1;;
            # Support aliases
            (p4d) ServerType="p4d_commit"; InstallP4D=1; InstallP4Broker=1; InstallP4Proxy=0;;
            (edge) ServerType="p4d_edge"; InstallP4D=1; InstallP4Broker=1; InstallP4Proxy=0;;
            (*) usage -h "Invalid server type specified: $2\\nValid values are:
* p4d_commit (or p4d_master, or p4d)
* p4d_replica
* p4d_filtered_replica
* p4d_edge (or edge)
* p4d_edge_replica
* p4p (or p4proxy)
* p4broker (or p4b)\\n";;
         esac
         shiftArgs=1
      ;;
      (-tp) TargetPort="$2"; TargetPortSet=1; shiftArgs=1;;
      (-lp) ListenPort="$2"; ListenPortSet=1; shiftArgs=1;;
      (-I)
         InitScripts="$2"
         shiftArgs=1
         [[ "$InitScripts" == *"p4p"* ]] && InstallP4Proxy=1
         [[ "$InitScripts" == *"dtg"* ]] && InstallP4DTG=1
      ;;
      (-MDD) OverrideDD="$2"; shiftArgs=1;;
      (-MCD) OverrideCD="$2"; shiftArgs=1;;
      (-MLG) OverrideLG="$2"; shiftArgs=1;;
      (-MDB1) OverrideDB1="$2"; shiftArgs=1;;
      (-MDB2) OverrideDB2="$2"; shiftArgs=1;;
      (-f) FastMode=1;;
      (-p) PreflightOnly=1;;
      (-no_init) DoServiceInit=0; UseSystemd=0;;
      (-no_systemd) UseSystemd=0;;
      (-no_enable) EnableSystemdServices=0;;
      (-fs) UpdateSudoers=1; LimitedSudoers=0;;
      (-ls) UpdateSudoers=1; LimitedSudoers=1;;
      (-no_cron) LoadActiveCrontab=0;;
      (-no_firewall) DoFirewall=0;;
      (-test) TestMode=1; SDPInstallRoot="/tmp/p4"; SDPMountPointBase="/tmp/hxmounts"; DoServiceInit=0;;
      (-clean) TestCleanup=1;;
      (-R) # Syntax: -R <alt_root>[:<alt_mount_base>]
         TestMode=1
         if [[ "$2" == *":"* ]]; then
            # alt_root is to the left of the colon, alt mount base is to the right.
            SDPInstallRoot="${2%%:*}"
            SDPMountPointBase="${2:##*:}"
         else
            SDPInstallRoot="$2"
            SDPMountPointBase=
         fi
         DoServiceInit=0
         shiftArgs=1
      ;;
      (-h) usage -h;;
      (-man) usage -man;;
      (-n) NoOp=1;;
      (-L) Log="$2"; shiftArgs=1;;
      (-d) Debug=1;;
      (-D) Debug=1; set -x;; # Debug; use bash 'set -x' high verbosity debugging mode.
      (-*) usage -h "Unknown flag/option ($1).";;
      (*)
         if [[ "$SDPInstance" == "UnsetSDPInstance" ]]; then
            SDPInstance="$1"
         else
            usage -h "Instance $1 specified after already being specified as $SDPInstance. Only one instance can be specified."
         fi
      ;;
   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

if [[ "$SDPInstance" == "UnsetSDPInstance" ]]; then
   usage -h "No instance specified."
fi

if [[ "$TargetPortSet" -eq 1 || "$ListenPortSet" -eq 1 ]]; then
   [[ "$ServerType" == "p4d"* ]] && usage "The '-tp <TargetPort>' and '-lp <ListenPort>' can only be used when ServerType is p4p or p4broker."
fi

if [[ "$UpdateSudoers" -eq 1 && "$RunningAsRoot" -eq 0 ]]; then
   if [[ "$LimitedSudoers" -eq 1 ]]; then
      usage -h "The '-ls' (limited sudoers) option was specified, but can only be used when running as root."
   else
      usage -h "The '-fs' (full sudoers) option was specified, but can only be used when running as root."
   fi
fi

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

# Pre-Preflight checks occur before logging starts, to ensure essential utils
# (including those needed for logging) are available in the PATH.
for util in awk date id rsync sed tee touch; do
   if ! command -v $util > /dev/null; then
      errmsg "Cannot find this essential util in PATH: $util"
   fi
done

if [[ "$ErrorCount" -ne 0 ]]; then
   bail "Aborting due to essential tools not available in directories listed in the PATH."
fi

[[ -n "$Log" ]] ||\
   Log="mkdirs.${SDPInstance}.$(date +'%Y%m%d-%H%M%S').log"

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

trap terminate EXIT SIGINT SIGTERM

# Redirect stdout and stderr to the log file.
if [[ "$Log" != off ]]; then
   exec > >(tee "$Log")
   exec 2>&1
   msg "Log is: $Log"
fi

ThisUser=$(whoami)

msg "Started $ThisScript version $Version as $ThisUser@$ThisHost on $(date) with this command line:\\n$CmdLine"

msg "Checking current umask."
InheritedUmask=$(umask)
[[ -n "$InheritedUmask" ]] || errmsg "Could not determine inherited umask."

if [[ "$InheritedUmask" == "$OperatingUmask" ]]; then
   msg "Verified: Umask is as expected: $OperatingUmask"
else
   umask $OperatingUmask || bail "Could not do: umask OperatingUmask"
   msg "Umask as inherited from environment was $InheritedUmask; temporarily changed umask to $OperatingUmask for this script's operations."
fi

# If /opt/perforce/helix-sdp exists, us it.
if [[ -d "$SDPPackageBase" ]]; then
   msg "Using SDP Package structure because $SDPPackageBase exists."
   UsePackageDirs=1
fi

TmpFile=$(mktemp)

# Standard Preflight checks after logging has started.
export SDP_INSTANCE="${SDPInstance}"

# If the config file wasn't specified, find it.
if [[ -z "$ConfigFile" ]]; then
   # Check for config file in same dir as this script, looking first for an
   # instance-specific variant.
   if [[ -n "$RunDir" ]]; then
      if [[ "$RunDir" == \. ]]; then
         GenSudoersScript="$PWD/gen_sudoers.sh"
      else
         GenSudoersScript="$RunDir/gen_sudoers.sh"
      fi
   else
      RunDir=.
      GenSudoersScript="$PWD/gen_sudoers.sh"
   fi

   if [[ -r "$RunDir/mkdirs.${SDPInstance}.cfg" ]]; then
      ConfigFile="$RunDir/mkdirs.${SDPInstance}.cfg"
   elif [[ -r "$SDPInstallRoot/common/site/config/mkdirs.${SDPInstance}.cfg" ]]; then
      ConfigFile="$SDPInstallRoot/common/site/config/mkdirs.${SDPInstance}.cfg"
   elif [[ -r "$SDPInstallRoot/common/config/mkdirs.${SDPInstance}.cfg" ]]; then
      ConfigFile="$SDPInstallRoot/common/config/mkdirs.${SDPInstance}.cfg"
   else
      ConfigFile="$RunDir/mkdirs.cfg"
   fi
fi

#------------------------------------------------------------------------------
# Load settings from the mkdirs config file.
if [[ -r "$ConfigFile" ]]; then
   msg "Loading mkdirs config file: $ConfigFile"
   # Note that CD must come after DD in this list below, because CD uses the value of DD as a default of CD is not defined.
   for c in DB1 DB2 DD CD LG SHAREDDATA MASTERINSTANCE OSUSER OSGROUP CASE_SENSITIVE ADMINUSER P4ADMINPASS DEFAULT_DOMAIN MAILFROM MAILTO MAILHOST SSL_PREFIX P4_PORT P4BROKER_PORT P4WEB_PORT P4FTP_PORT P4P_TARGET_PORT P4MASTERHOST PERMS MASTER_ID SERVER_TYPE REPLICA_ID COMPLAINFROM_DOMAIN COMPLAINFROM; do
      value=$(grep ^$c= "$ConfigFile")
      if [[ -n "$value" ]]; then
         value="${value#*=}"

         # Encode passwords so that we can accept a complex string value, i.e.
         # one that includes a literal value that looks like a shell environment
         # variable, e.g.: P4ADMINPASS=Complex$u.t$a!#%3$CRx
         if [[ "$c" == P4ADMINPASS ]]; then
            eval export $c="$(echo -n "$value" | base64 -)"
         else
            eval export $c="$value"
         fi

         # Perform value sanity check verifications. For SERVER_TYPE, also do implicit conversion from types
         # specified in mkrep.sh.
         case "$c" in
            (SERVER_TYPE)
               # Convert all unfiltered replica types to p4d_replica.
               [[ "$value" =~ ^(p4d_ha|p4d_ham|p4d_ro|p4d_rom|p4d_fr|p4d_frm|p4d_fs|p4d_fsm)$ ]] && \
                  value="p4d_replica"

               # Convert all filtered replica types to p4d_filtered_replica.
               [[ "$value" =~ ^(p4d_ffr)$ ]] && \
                  value="p4d_filtered_replica"

               [[ "$value" =~ ^(p4d_commit|p4d_master|p4d_replica|p4d_filtered_replica|p4d_edge|p4d_edge_replica|p4broker|p4p|p4proxy)$ ]] || \
                  errmsg "SERVER_TYPE value of [$value] is invalid.  Set to one of: p4d_commit,p4d_master,p4d_replica,p4d_filtered_replica,p4d_edge,p4d_edge_replica,p4broker,p4p,p4proxy."
            ;;
            (PERMS)
               [[ "$value" =~ ^(Owner|Group|Other)$ ]] || \
                  errmsg "PERMS value of [$value] is invalid.  Set to one of: Owner, Group, or Other."
            ;;
         esac
      else
         # Silently ignore if optional settings aren't set; give an error if required settings are missing.
         # The COMPLAINFROM, COMPLAINFROM_DOMAIN, and other settings are optional.
         [[ "$c" =~ (COMPLAINFROM|COMPLAINFROM_DOMAIN|MAILHOST|P4FTP_PORT|P4P_TARGET_PORT|P4WEB_PORT|PERMS) ]] && \
            continue
         if [[ "$c" == "REPLICA_ID" && "$SERVER_TYPE" =~ ^(p4d_replica|p4d_filtered_replica|p4d_edge|p4d_edge_replica)$ ]]; then
            errmsg "Missing REPLICA_ID (required as SERVER_TYPE is $SERVER_TYPE) in mkdirs config file: $ConfigFile"
         # The 'CD' setting is optional. If it is not defined, set it to the same value as DD which is already defined at this point.
         elif [[ "$c" == "CD" ]]; then
            dbg "CD was not defined, so it was set to the default value of DD [$DD]."
            export CD="$DD"
         else
            errmsg "Missing required setting [$c] in mkdirs config file: $ConfigFile"
         fi
     fi
   done
else
   errmsg "Missing mkdirs config file: $ConfigFile"
fi

#------------------------------------------------------------------------------
# Handle command line overrides of certain settings in the config file.

# If '-s <ServerID>' was specified on the command line, override REPLICA_ID.
if [[ -n "$ServerID" ]]; then
   if [[ "$ServerID" == "$REPLICA_ID" ]]; then
      msg "\\nUsing ServerID [$ServerID] due to '-s $ServerID'. This matches REPLICA_ID value from config file."
   else
      msg "\\nUsing ServerID [$ServerID] due to '-s $ServerID'. Ignoring REPLICA_ID value from config file [$REPLICA_ID]."
   fi
else
   ServerID="$REPLICA_ID"
   msg "Using ServerID value from REPLICA_ID setting in config file [$ServerID]."
fi

# If '-t <ServerType>' was specified on the command line, override SERVER_TYPE.
if [[ -n "$ServerType" ]]; then
   if [[ "$ServerType" == "$SERVER_TYPE" ]]; then
      msg "\\nUsing Server Type [$ServerType] due to '-t $ServerType'. This matches SERVER_TYPE value from config file."
   else
      msg "\\nUsing Server Type [$ServerType] due to '-t $ServerType'. Ignoring SERVER_TYPE value from config file [$SERVER_TYPE]."
   fi
else
   ServerType="$SERVER_TYPE"
   msg "Using ServerType value from SERVER_TYPE setting in config file [$ServerType]."
fi

[[ "$ServerType" == "p4d_edge_replica" && -z "$TargetServerID" ]] && \
   errmsg "ServerType is p4d_edge_replica but '-S <TargetServerID>' is not set."

[[ ("$ServerType" =~ ^(p4d_replica|p4d_filtered_replica|p4d_edge|p4d_edge_replica)$) && ("$REPLICA_ID" == "$MASTER_ID") ]] && \
   errmsg "ServerType is $ServerType but the REPLICA_ID value is the same as the MASTER_ID." 

#------------------------------------------------------------------------------
# Determine what services to install based on ServerType.
case "$ServerType" in
   (p4d_commit|p4d_master|p4d_replica|p4d_filtered_replica|p4d_edge|p4d_edge_replica)
      InstallP4D=1
      InstallP4Broker=1
      InstallP4Proxy=0
   ;;
   (p4broker|p4b) InstallP4D=0; InstallP4Broker=1; InstallP4Proxy=0;;
   (p4p|p4proxy) InstallP4D=0; InstallP4Broker=0; InstallP4Proxy=1;;
   (*) errmsg "Unknown ServerType: $ServerType";;
esac

[[ "$InstallP4D" -eq 1 ]] && msg "A p4d will be installed for instance $SDPInstance."
[[ "$InstallP4Broker" -eq 1 ]] && msg "A p4broker will be installed for instance $SDPInstance."
[[ "$InstallP4Proxy" -eq 1 ]] && msg "A p4p will be installed for instance $SDPInstance."

#------------------------------------------------------------------------------
# Determine SDP "TarRoot".  For greenfield installations, this is where the
# SDP tarball was extracted (usually /hxdepots/sdp) or for package installs,
# the /opt/perforce/helix-sdp/sdp directory -- in either case the clean,
# unmodified SDP structure, without generated files or symlinks.  For
# reliability in non-package installs, this directory is calculated as
# 3 levels above the current directory where mkdirs.sh is run from.
#
# This mkdirs.sh script is run from <Base>/Server/Unix/setup, where <Base> is
# usually /hxdepots/sdp or /opt/perforce/helix-sdp/sdp for package
# installations. Set TarRoot and several values derived from it.
if [[ "$UsePackageDirs" -eq 1 ]]; then
   TarRoot="$ImmutableSDPDir"
else
   TarRoot="$(pwd -P)"
   TarRoot="${TarRoot%/*}"
   TarRoot="${TarRoot%/*}"
   TarRoot="${TarRoot%/*}"
fi

TarRootCommon="$TarRoot/Server/Unix/p4/common"
TarRootConfig="$TarRoot/Server/Unix/p4/common/config"
TarRootCron="$TarRoot/Server/Unix/p4/common/etc/cron.d"
TarRootInit="$TarRoot/Server/Unix/p4/common/etc/init.d"
TarRootSSL="$TarRoot/Server/Unix/p4/ssl"
TarRootUnsupported="$TarRoot/Unsupported"

# The helix_binaries dir is parallel to, not under, the package root dir.
if [[ "$UsePackageDirs" -eq 1 ]]; then
   TarRootBinDir="${TarRoot%/*}/helix_binaries"
else
   TarRootBinDir="$TarRoot/helix_binaries"
fi

ExtendPermsTo="${PERMS:-Owner}"

case "${ExtendPermsTo}" in
   (Owner) ExecPerms=700; ReadPerms=600;;
   (Group) ExecPerms=750; ReadPerms=640;;
   (Other) ExecPerms=755; ReadPerms=644;;
esac

#------------------------------------------------------------------------------
# Do preflight checks, starting with the SDP version check.
SDPVersionFile="$TarRoot/Version"
if [[ -r "$SDPVersionFile" ]]; then
   SDPVersionString="$(cat "$SDPVersionFile")"
   SDPVersion="$(get_sdp_version_from_string "$SDPVersionString")"
   msg "SDP Version from $SDPVersionFile is: $SDPVersion"
else
   errmsg "Preflight check failed: Missing SDP Version file: $SDPVersionFile"
fi

[[ -n "$OverrideDD" ]] && DD="$OverrideDD"
[[ -n "$OverrideCD" ]] && CD="$OverrideCD"
[[ -n "$OverrideLG" ]] && LG="$OverrideLG"
[[ -n "$OverrideDB1" ]] && DB1="$OverrideDB1"
[[ -n "$OverrideDB2" ]] && DB2="$OverrideDB2"

# If -MDB1 is specified, and -MDB2 is not, use the value DB1 value for DB2.
[[ -n "$OverrideDB1" && -z "$OverrideDB2" ]] && DB2="$OverrideDB1"

#------------------------------------------------------------------------------
# Normalizations: Trim the leading '/' from DB1, DB2, LG, DD and CD values. These
# normalizations allow the values as specified in the configuration file to be
# more flexible.  That is, absolute paths with the leading '/' can be
# specified, which tend to be more readable and familiar to humans reading the
# config file, since the values match the actual mount point names. The older
# format, with the leading slash removed, is still accepted for backward
# compatibility.

DD="${DD#/}"
CD="${CD#/}"
LG="${LG#/}"
DB1="${DB1#/}"
DB2="${DB2#/}"

P4SERVER="p4_$SDPInstance"

export MAIL=mail

if [[ "$RunningAsRoot" -eq 1 ]]; then
   msg "Verified: Running as root."
elif [[ $(id -u -n) == "$OSUSER" ]]; then
   warnmsg "Not running as root. The chown commands will be skipped and basic directories must exist.\\n"
   DoChownCommands=0
   DoNFSChownCommands=0
else
   errmsg "$0 must be run as root or $OSUSER.\\n"
fi

#------------------------------------------------------------------------------
# Handle -test mode.
if [[ "$TestMode" -eq 1 ]]; then
   DB1="${SDPMountPointBase#/}/$DB1"
   DB2="${SDPMountPointBase#/}/$DB2"
   DD="${SDPMountPointBase#/}/$DD"
   CD="${SDPMountPointBase#/}/$CD"
   LG="${SDPMountPointBase#/}/$LG"

   msg "\\n********* -test specified *********\\n"
   msg "SDP  Dir: $TarRoot"
   msg "Override for SDP  Root: $SDPInstallRoot"
   msg "Override for SDP Mount Point Base: $SDPMountPointBase\\n"
   msg "DD=$DD\\nCD=$CD\\nLG=$LG\\nDB1=$DB1\\nDB2=$DB2\\n"

   if [[ "$TestCleanup" -eq 1 ]]; then
      msg "Test Cleanup specified with -clean."
      DirList="$SDPInstallRoot $SDPMountPointBase"

      for d in $DirList; do
         if [[ -d "$d" ]]; then
            run "rm -rf $d" "Cleanup: Removing test dir from prior test: $d" ||\
               errmsg "Failed to remove test dir from prior test: $d"
         fi
      done
   fi
fi

# If we aren't running as root, extend the preflight checks to verify basic dirs
# exist.
if [[ $(id -u) -ne 0 ]]; then
   DirList="$SDPInstallRoot"
   DirList+=" /$DD /$LG"
   if [[ "$ServerType" == "p4d"* ]]; then
      DirList+=" /$DB1"
      [[ "$DB1" == "$DB2" ]] || DirList+=" /$DB2"
      [[ "$DD" == "$CD" ]] || DirList+=" /$CD"
   fi

   for d in $DirList; do
      if [[ -d "$d" ]]; then
         dirOwner=$(stat -c "%U" "$d")
         dirGroup=$(stat -c "%G" "$d")
         [[ "$dirOwner" == "$OSUSER" ]] ||\
            errmsg "Dir [$d] exists but with wrong owner, $dirOwner instead of $OSUSER."
         [[ "$dirGroup" == "$OSGROUP" ]] ||\
            warnmsg "Dir [$d] exists but with wrong group, $dirGroup instead of $OSGROUP."
      else
         errmsg "Dir must exist if not running as root: $d"
      fi
   done
fi

if [[ "$UsePackageDirs" -eq 1 ]]; then
   dirOwner=$(stat -c "%U" "$ImmutableSDPDir")
   dirGroup=$(stat -c "%G" "$ImmutableSDPDir")
   dirPerms=$(stat -c "%a" "$ImmutableSDPDir")
   if [[ "$dirOwner" == root && "$dirGroup" == root && "$dirPerms" == 755 ]]; then
      msg "Verified: ownership and perms for dir $ImmutableSDPDir are correct, root:root, 755."
   else
      errmsg "Ownership and perms for dir $ImmutableSDPDir should be root:root, 755 but are $dirOwner:$dirGroup, $dirPerms."
   fi

   dirOwner=$(stat -c "%U" "$SDPPackageBase")
   dirGroup=$(stat -c "%G" "$SDPPackageBase")
   dirPerms=$(stat -c "%a" "$SDPPackageBase")
   if [[ "$dirOwner" == root && "$dirGroup" == "$OSGROUP" && "$dirPerms" == 775 ]]; then
      msg "Verified: ownership and perms for dir $SDPPackageBase are correct, root:$OSGROUP, 775."
   else
      errmsg "Ownership and perms for dir $SDPPackageBase should be root:$OSGROUP, 775 but are $dirOwner:$dirGroup, $dirPerms."
   fi
fi

if [[ "$ErrorCount" -eq 0 ]]; then
   msg "Verified: Preflight checks passed ($WarningCount warnings)."
else
   bail "Aborting due to failed preflight checks."
fi

if [[ "$UsePackageDirs" -eq 0 ]]; then
   # Check for required and optional binaries.
   [[ -f "$TarRootBinDir/p4" ]] || errmsg "No p4 in $TarRootBinDir/"

   if [[ "$ServerType" == "p4d"* ]]; then
      [[ -f "$TarRootBinDir/p4d" ]] || errmsg "No p4d in $TarRootBinDir/"
   else
      [[ -f "$TarRootBinDir/p4d" ]] || warnmsg "No p4d in $TarRootBinDir/"
   fi

   # We allow 'p4proxy' as a synonmym for 'p4p'; the binary is p4p.
   if [[ "$ServerType" == "p4p"* ]]; then
      [[ -f "$TarRootBinDir/p4p" ]] || errmsg "No p4p in $TarRootBinDir/"
   else
      [[ -f "$TarRootBinDir/p4p" ]] || warnmsg "No p4p in $TarRootBinDir/"
   fi

   if [[ "$ServerType" == "p4broker" ]]; then
      [[ -f "$TarRootBinDir/p4broker" ]] || errmsg "No p4broker in $TarRootBinDir/"
   else
      [[ -f "$TarRootBinDir/p4broker" ]] || warnmsg "No p4broker in $TarRootBinDir/"
   fi
fi

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

#------------------------------------------------------------------------------
# Preflight checks completed. Continue on!

#------------------------------------------------------------------------------
# Acquire helix_binaries as needed.

msg "\\nAcquiring Helix Binaries from FTP server if needed."

if [[ "$UsePackageDirs" -eq 1 ]]; then
   if [[ ! -d "$SDPPackageBins" ]]; then
      run "rsync -a $ImmutableSDPDir/helix_binaries/ $SDPPackageBins" \
         "Copying to $SDPPackageBins" ||\
         errmsg "Failed to copy to $SDPPackageBins."
   else
      dbg "Using existing directory: $SDPPackageBins"
   fi

   for binary in p4 p4d p4broker p4p; do
      if [[ -r "$SDPPackageBins/$binary" ]]; then
         msg "Using existing binary: $SDPPackageBins/$binary"
      else
         # If no version was explicitly specified for Helix Binaries, use the
         # default version defined in install_sdp.sh.
         if [[ -z "$P4BinRel" && -r "./install_sdp.sh" ]]; then
            P4BinRel="$(grep '^declare DefaultP4BinRel=' ./install_sdp.sh | cut -d= -f2)"
         fi

         if [[ -n "$P4BinRel" ]]; then
            run "$SDPPackageBins/get_helix_binaries.sh -sbd $SDPPackageBins -b $binary -r $P4BinRel" \
               "Acquiring binary $SDPPackageBins/$binary version $P4BinRel." ||\
               errmsg "Failed to acquire binary $SDPPackageBins/$binary version $P4BinRel."
         else
            run "$SDPPackageBins/get_helix_binaries.sh -sbd $SDPPackageBins -b $binary" \
               "Acquiring binary $SDPPackageBins/$binary." ||\
               errmsg "Failed to acquire binary $SDPPackageBins/$binary."
         fi
      fi
   done
fi

run "chmod 755 $TarRootBinDir/p4" || errmsg "Failed to chmod p4 binary."

# shellcheck disable=SC2015
[[ -f "$TarRootBinDir/p4d" ]] && run "chmod $ExecPerms $TarRootBinDir/p4d" ||\
   errmsg "Failed to chmod p4d binary."

# shellcheck disable=SC2015
[[ -f "$TarRootBinDir/p4broker" ]] && run "chmod $ExecPerms $TarRootBinDir/p4broker" ||\
   errmsg "Failed to chmod p4broker binary."

# shellcheck disable=SC2015
[[ -f "$TarRootBinDir/p4p" ]] && run "chmod $ExecPerms $TarRootBinDir/p4p" ||\
   errmsg "Failed to chmod p4p binary."

#------------------------------------------------------------------------------
# Calculate binary versions.
P4RELNUM=$("$TarRootBinDir/p4" -V | grep -i Rev. | awk -F / '{print $3}')
P4BLDNUM=$("$TarRootBinDir/p4" -V | grep -i Rev. | awk -F / '{print $4}' | awk '{print $1}')

if [[ "$ServerType" == "p4d"* ]]; then
   P4DRELNUM=$("$TarRootBinDir/p4d" -V | grep -i Rev. | awk -F / '{print $3}')
   P4DBLDNUM=$("$TarRootBinDir/p4d" -V | grep -i Rev. | awk -F / '{print $4}' | awk '{print $1}')
fi

#------------------------------------------------------------------------------
# Start installation.
msg "\\nCreating/verifying SDP directories."

if [[ "$UsePackageDirs" -eq 1 ]]; then
   DirList="$SDPInstallRoot /$DD/p4 /$LG/p4 $WritableP4Dir/common/bin $WritableP4Dir/common/config"
else
   DirList="$SDPInstallRoot /$DD/p4 /$LG/p4 /$DD/p4/common/bin /$DD/p4/common/config"
fi

if [[ "$ServerType" == "p4d"* ]]; then
   DirList+=" /$DB1/p4"
   [[ "$DB1" == "$DB2" ]] || DirList+=" /$DB2/p4"
   [[ "$DD" == "$CD" ]] || DirList+=" /$CD/p4"
fi

for d in $DirList; do
   if [[ ! -d "$d" ]]; then
      run "mkdir -p $d" "Creating initial install dir: $d" ||\
         errmsg "Failed to create initial install dir: $d"
   fi
done

DirList="$SDPInstallRoot/ssl /$LG/p4/$SDPInstance/tmp"
if [[ "$ServerType" == "p4d"* ]]; then
   DirList+=" /$DD/p4/$SDPInstance/depots /$CD/p4/$SDPInstance/checkpoints"

   if [[ "$ServerType" == "p4d_edge" || "$ServerType" == "p4d_filtered_replica" || "$SHAREDDATA" == "TRUE" ]]; then
      DirList="$DirList /$CD/p4/$SDPInstance/checkpoints.${ServerID#p4d_}"
   elif [[ "$ServerType" == "p4d_edge_replica" ]]; then
      DirList="$DirList /$CD/p4/$SDPInstance/checkpoints.${TargetServerID#p4d_}"
   fi
elif [[ "$ServerType" == "p4p"* ]]; then
   DirList+=" /$DD/p4/$SDPInstance/cache"
fi

for d in $DirList; do
   if [[ ! -d "$d" ]]; then
      run "mkdir -p $d" "Creating install subdir: $d" ||\
         errmsg "Failed to create install subdir: $d"
   fi
done

if [[ ! -d "$SDPInstallRoot/$SDPInstance" ]]; then
   run "mkdir $SDPInstallRoot/$SDPInstance" "Creating dir for instance: $SDPInstallRoot/$SDPInstance" ||\
      errmsg "Failed to create dir for instance $SDPInstallRoot/$SDPInstance."
fi

#------------------------------------------------------------------------------
# ServerID handling.
if [[ "$ServerType" == "p4d_edge" || "$ServerType" == "p4d_filtered_replica" ]]; then
   if [[ "$DoChownCommands" -eq 1 ]]; then
      run "chown $OSUSER:$OSGROUP /$CD/p4/$SDPInstance/checkpoints.${ServerID#p4d_}" \
         "Adjusting ownership of checkpoints.${ServerID#p4d_}."
   fi
   run "ln -f -s /$CD/p4/$SDPInstance/checkpoints.${ServerID#p4d_} $SDPInstallRoot/$SDPInstance/checkpoints.${ServerID#p4d_}" \
      "Creating symlink for $SDPInstallRoot/$SDPInstance/checkpoints.${ServerID#p4d_}" ||\
      errmsg "Failed to create symlink."

   if [[ "$DoChownCommands" -eq 1 ]]; then
      run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot/$SDPInstance/checkpoints.${ServerID#p4d_}" \
         "Adjusting ownership of symlink to checkpoints.${ServerID#p4d_}."
   fi
elif [[ "$ServerType" == "p4d_edge_replica" ]]; then
   if [[ "$DoChownCommands" -eq 1 ]]; then
      run "chown $OSUSER:$OSGROUP /$CD/p4/$SDPInstance/checkpoints.${TargetServerID#p4d_}" \
         "Adjusting ownership of checkpoints.${TargetServerID#p4d_}."
   fi
   run "ln -f -s /$CD/p4/$SDPInstance/checkpoints.${TargetServerID#p4d_} $SDPInstallRoot/$SDPInstance/checkpoints.${TargetServerID#p4d_}" \
      "Creating symlink for $SDPInstallRoot/$SDPInstance/checkpoints.${TargetServerID#p4d_}" ||\
      errmsg "Failed to create symlink."

   if [[ "$DoChownCommands" -eq 1 ]]; then
      run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot/$SDPInstance/checkpoints.${TargetServerID#p4d_}" \
         "Adjusting ownership of symlink to checkpoints.${TargetServerID#p4d_}."
   fi
fi

if [[ -f "$TarRootSSL/config.txt" && ! -f "$SDPInstallRoot/ssl/config.txt" ]]; then
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      run "cp $ImmutableSDPDir/Server/Unix/p4/ssl/config.txt $SDPInstallRoot/ssl/." \
         "Copying SSL cert generation file config.txt to $SDPInstallRoot/ssl." ||\
         errmsg "Failed to copy $TarRootSSL/config.txt to $SDPInstallRoot/ssl/config.txt."
   else
      run "cp $TarRootSSL/config.txt $SDPInstallRoot/ssl/." \
         "Copying SSL cert generation file config.txt to $SDPInstallRoot/ssl." ||\
         errmsg "Failed to copy $TarRootSSL/config.txt to $SDPInstallRoot/ssl/config.txt."
   fi
else
   msg "Using existing $SDPInstallRoot/ssl directory."
fi

#------------------------------------------------------------------------------
# Add/verify /p4/sdp symlink.
msg "\\nAdding/Verifying SDP symlink: $SDPInstallRoot/sdp"

if [[ -L "$SDPInstallRoot/sdp" ]]; then
   SymlinkTarget=$(readlink "$SDPInstallRoot/sdp")
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      if [[ "$SymlinkTarget" == "$WritableSDPDir" ]]; then
         msg "Verified: $SDPInstallRoot/sdp is a symlink to $WritableSDPDir."
      else
         warnmsg "Symlink $SDPInstallRoot/sdp should be to $WritableSDPDir, but instead points to [$SymlinkTarget]."
      fi
   else
      if [[ "$SymlinkTarget" == "$TarRoot" ]]; then
         msg "Verified: $SDPInstallRoot/sdp is a symlink to $TarRoot."
      else
         warnmsg "Symlink $SDPInstallRoot/sdp should be to $TarRoot, but instead points to [$SymlinkTarget]."
      fi
   fi
elif [[ -e "$SDPInstallRoot/sdp" ]]; then
   errmsg "This path is expected to be a symlink but is not: $SDPInstallRoot/sdp"
else
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      run "ln -s $WritableSDPDir $SDPInstallRoot/sdp" "Creating symlink to SDP package install area." ||\
         errmsg "Failed to make symlink for sdp."
   else
      run "ln -s $TarRoot $SDPInstallRoot/sdp" "Creating symlink to SDP package install area." ||\
         errmsg "Failed to make symlink for sdp."
   fi
fi

#------------------------------------------------------------------------------
# Add/verify /p4/common symlink.
if [[ -L "$SDPInstallRoot/common" ]]; then
   SymlinkTarget=$(readlink "$SDPInstallRoot/common")
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      if [[ "$SymlinkTarget" == "$WritableSDPDir/Server/Unix/p4/common" ]]; then
         msg "Verified: The $SDPInstallRoot/common symlink references $WritableSDPDir/Server/Unix/p4/common, as expected (because $ImmutableSDPDir exists)."
      elif [[ "$SymlinkTarget" == "/$DD/p4/common" ]]; then
         warnmsg "The $SDPInstallRoot/common symlink references the non-package path /$DD/p4/common; it is expected to reference $WritableSDPDir/Server/Unix/p4/common (because $ImmutableSDPDir exists)."
      elif [[ "$SymlinkTarget" == "/$DD/p4/common" ]]; then
         warnmsg "The $SDPInstallRoot/common symlink references an unexpected target path [$SymlinkTarget]."
      fi
   else
      if [[ "$SymlinkTarget" == "/$DD/p4/common" ]]; then
         msg "Verified: The $SDPInstallRoot/common symlink references /$DD/p4/common, as expected (because $ImmutableSDPDir does not exist)."
      elif [[ "$SymlinkTarget" == "ImmutableSDPDir/common" ]]; then
         warnmsg "The $SDPInstallRoot/common symlink references $ImmutableSDPDir/common; it is expected to reference /$DD/p4/common (because $ImmutableSDPDir does not exist)."
      elif [[ "$SymlinkTarget" == "/$DD/p4/common" ]]; then
         warnmsg "The $SDPInstallRoot/common symlink references an unexpected target path [$SymlinkTarget]."
      fi
   fi
elif [[ -e "$SDPInstallRoot/common" ]]; then
   errmsg "This path is expected to be a symlink but is not: $SDPInstallRoot/common"
else
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      run "ln -s $WritableSDPDir/Server/Unix/p4/common $SDPInstallRoot/common" \
         "Creating symlink for common dir." ||\
         errmsg "Failed to create symlink for common dir."
   else
      run "ln -s /$DD/p4/common $SDPInstallRoot/common" \
         "Creating symlink for common dir." ||\
         errmsg "Failed to create symlink for common dir."
   fi

   if [[ "$DoChownCommands" -eq 1 ]]; then
      run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot/common" \
         "Adjusting ownership of symlink to $SDPInstallRoot/common" ||\
         errmsg "Failed to adjust ownership of symlink to $SDPInstallRoot/common"
   fi
fi

#------------------------------------------------------------------------------
# Add /hxlogs directories.
DirList="/$LG/p4/$SDPInstance/logs /$LG/p4/$SDPInstance/tmp"

if [[ "$ServerType" == "p4d"* ]]; then
   DirList+=" /$DB1/p4/$SDPInstance/db1/save /$DB2/p4/$SDPInstance/db2/save"
fi

for d in $DirList; do
   if [[ ! -d "$d" ]]; then
      run "mkdir -p $d" "Creating dir: $d" ||\
         errmsg "Failed to create dir: $d"
   fi
done

#------------------------------------------------------------------------------
# Working in /p4/N, add bin dir and symlinks as needed.

# Test a cd to /$DD/p4/<instance>, unless on a broker only host.
if [[ "$NoOp" -eq 0 && "$ServerType" != "p4broker" ]]; then
   cd "/$DD/p4/$SDPInstance" || errmsg "Could not do: cd /$DD/p4/$SDPInstance"
fi

# Test cd to /p4
if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot" || errmsg "Could not do: cd $SDPInstallRoot"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot"
fi

if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstance" || bail "Could not do: cd $SDPInstance (from directory: $SDPInstallRoot)"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance"
fi

if [[ ! -d bin ]]; then
   run "mkdir bin" || errmsg "Failed to do: mkdir bin"
fi

if [[ "$InstallP4D" -eq 1 ]]; then
   if [[ ! -L root ]]; then
      run "ln -s /$DB1/p4/$SDPInstance/db1 root" ||\
         errmsg "Failed to make symlink for P4ROOT."
   fi

   if [[ ! -L offline_db ]]; then
      run "ln -s /$DB2/p4/$SDPInstance/db2 offline_db" ||\
         errmsg "Failed to make symlink for offline_db."
   fi

   if [[ ! -L depots ]]; then
      run "ln -s /$DD/p4/$SDPInstance/depots" ||\
         errmsg "Failed to make symlink for depots."
   fi

   if [[ ! -L checkpoints ]]; then
      run "ln -s /$CD/p4/$SDPInstance/checkpoints" ||\
         errmsg "Failed to make symlink for checkpoints."
   fi

   if [[ "$ServerType" == "p4d_edge" || "$ServerType" == "p4d_filtered_replica" ]]; then
      if [[ ! -L "checkpoints.${ServerID#p4d_}" ]]; then
         run "ln -s /$CD/p4/$SDPInstance/checkpoints.${ServerID#p4d_}" ||\
            errmsg "Failed to make symlink for checkpoints.${ServerID#p4d_}."
      fi
   elif [[ "$ServerType" == "p4d_edge_replica" ]]; then
      if [[ ! -L "checkpoints.${TargetServerID#p4d_}" ]]; then
         run "ln -s /$CD/p4/$SDPInstance/checkpoints.${TargetServerID#p4d_}" ||\
            errmsg "Failed to make symlink for checkpoints.${TargetServerID#p4d_}."
      fi
   fi
fi

#------------------------------------------------------------------------------
# Add /p4/N/logs and /p4/N/tmp symlinks on /hxlogs for all server types.

for d in logs tmp; do
   if [[ ! -d "$d" ]]; then
      if [[ ! -L "$d" ]]; then
         run "ln -s /$LG/p4/$SDPInstance/$d" ||\
            errmsg "Failed to make symlink for /p4/$SDPInstance/$d."
      fi
   fi
done

#------------------------------------------------------------------------------
# Set ServerID for all server types.
# For all p4d* type servers, add $P4ROOT/server.id, with commit server using
# COMMIT_ID or SERVER_ID.
case "$ServerType" in
   (p4d_commit|p4d_master)
      # If the ServerID was not set by the user with '-s', then set a
      # default value for a master.
      if [[ "$ServerIDSet" -eq 0 ]]; then
         ServerID="${COMMIT_ID:-${MASTER_ID:-UNSET_SERVERID}}"
      fi
      if [[ "$NoOp" -eq 0 ]]; then
         echo "$ServerID" > "$SDPInstallRoot/$SDPInstance/root/server.id"
      else
         msg "NO_OP: Would write $ServerID into $SDPInstallRoot/$SDPInstance/root/server.id"
      fi
   ;;
   (p4d_*)
      if [[ "$NoOp" -eq 0 ]]; then
         echo "$ServerID" > "$SDPInstallRoot/$SDPInstance/root/server.id"
      else
         msg "NO_OP: Would write $ServerID into $SDPInstallRoot/$SDPInstance/root/server.id"
      fi
   ;;
   (*)
      # If the ServerID was not set by the user with '-s', then use ServerType
      # as default value for the ServerID of a broker/proxy.
      if [[ "$ServerIDSet" -eq 0 ]]; then
         ServerID="$ServerType"
      fi
   ;;
esac

#------------------------------------------------------------------------------
# Initialize directories.
#
# For package installations, the /opt/perforce/helix-sdp/sdp structure is
# root-owned and immutable except by package upgrades.
# This block creates locally modifiable subdirs common, downloads,
# helix_binaries, and p4/sdp folders under /opt/perforce/helix-sdp, copying
# from the immutable area.
#
# When used with the install_sdp.sh script, some of this work will already
# be done. This mkdirs.sh script only creates/populates directories that do
# not already exist.
if [[ "$UsePackageDirs" -eq 1 ]]; then
   DirList="downloads helix_binaries p4/sdp/Server/Unix/p4/common"

   for d in $DirList; do
      if [[ ! -d "$d" ]]; then
         run "mkdir -p $SDPPackageBase/$d" \
            "Creating locally modifiable dir $SDPPackageBase/$d" ||\
            errmsg "Failed to create dir $SDPPackageBase/$d"
      else
         dbg "No need to create existing directory: $SDPPackageBase/$d"
      fi
   done

   if [[ ! -d "$WritableSDPDir/Server/Unix/p4/common" ]]; then
      run "rsync -a $ImmutableSDPDir/Server/Unix/p4/common/ $WritableSDPDir/Server/Unix/p4/common" \
         "Copying to $WritableSDPDir/Server/Unix/p4/common" ||\
         errmsg "Failed to copy to $WritableSDPDir/Server/Unix/p4/common"
   else
      dbg "Using existing directory: $WritableSDPDir/Server/Unix/p4/common"
   fi

   if [[ ! -f "$WritableP4Dir/Version" ]]; then
      run "rsync $ImmutableSDPDir/Version $WritableP4Dir/Version" \
         "Copying to $WritableP4Dir/Version" ||\
         errmsg "Failed to copy to $WritableP4Dir/Version"
   else
      dbg "Using existing file: $WritableP4Dir/Version"
   fi
else
   # The legacy, non-package logic is preserved for non-package installations.
   # It does not use rsync, and only executes if # /p4/common/bin/p4_vars does
   # not yet exist.
   if [[ ! -f "$SDPInstallRoot/common/bin/p4_vars" ]]; then
      run "cp -R $TarRootCommon/bin/* $SDPInstallRoot/common/bin" "Copying $SDPInstallRoot/common/bin" ||\
         errmsg "Failed to copy to $SDPInstallRoot/common/bin"

      # Copy certain subdirs of /p4/common if don't already exist.
      for d in cloud etc lib site; do
         if [[ ! -d "$SDPInstallRoot/common/$d" ]]; then
            run "cp -pr $TarRootCommon/$d $SDPInstallRoot/common/." "Copying $SDPInstallRoot/common/$d." ||\
               errmsg "Failed to copy $d to $SDPInstallRoot/common/"
         fi
      done
   fi
fi

#------------------------------------------------------------------------------
# Operate in /p4/common/bin
if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/common/bin" ||\
     bail "Could not do: cd $SDPInstallRoot/common/bin"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/common/bin"
fi

#------------------------------------------------------------------------------
# Processing for new installations that don't yet have /p4/common/bin/p4_vars.
if [[ ! -f "$SDPInstallRoot/common/bin/p4_vars" ]]; then
   if [[ ! -L "p4_${P4RELNUM}_bin" ]]; then
      run "ln -s p4_$P4RELNUM.$P4BLDNUM p4_${P4RELNUM}_bin" "Linking p4_${P4RELNUM}_bin." ||\
         errmsg "Failed to symlink p4_${P4RELNUM}_bin"
   fi

   if [[ "$ServerType" == "p4d"* ]]; then
      if [[ ! -L "p4d_${P4DRELNUM}_bin" ]]; then
         run "ln -s p4d_$P4DRELNUM.$P4DBLDNUM p4d_${P4DRELNUM}_bin" "Linking p4d_${P4DRELNUM}_bin." ||\
            errmsg "Failed to symlink p4_${P4DRELNUM}_bin"
      fi
   fi

   if [[ ! -L "p4_bin" ]]; then
      run "ln -s p4_${P4RELNUM}_bin p4_bin" "Linking p4_bin." ||\
         errmsg "Failed to symlink p4_bin"
   fi

   if [[ ! -f p4_vars ]]; then
      msg "Generating Common Environment File: $PWD/p4_vars"
      if [[ "$NoOp" -eq 0 ]]; then
         sed -e "s|REPL_SDPVERSION|${SDPVersionString}|g" \
            -e "s|REPL_OSUSER|${OSUSER}|g" \
            "$TarRootConfig/p4_vars.template" > p4_vars
      fi
   fi
fi

#------------------------------------------------------------------------------
# Copy version-tagged binaries to /p4/common/bin.
if [[ ! -f "$SDPInstallRoot/common/bin/p4_$P4RELNUM.$P4BLDNUM" ]]; then
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      run "cp $SDPPackageBins/p4 $SDPInstallRoot/common/bin/p4_$P4RELNUM.$P4BLDNUM" ||\
         bail "Failed to copy p4 binary."
   else
      run "cp $TarRootBinDir/p4 $SDPInstallRoot/common/bin/p4_$P4RELNUM.$P4BLDNUM" ||\
         bail "Failed to copy p4 binary."
   fi
fi

if [[ "$ServerType" == "p4d"* ]]; then
   if [[ ! -f "$SDPInstallRoot/common/bin/p4d_$P4DRELNUM.$P4DBLDNUM" ]]; then
      if [[ "$UsePackageDirs" -eq 1 ]]; then
         run "cp $SDPPackageBins/p4d $SDPInstallRoot/common/bin/p4d_$P4DRELNUM.$P4DBLDNUM" ||\
            bail "Failed to copy p4d binary"
      else
         run "cp $TarRootBinDir/p4d $SDPInstallRoot/common/bin/p4d_$P4DRELNUM.$P4DBLDNUM" ||\
            bail "Failed to copy p4d binary"
      fi
      # shellcheck disable=SC2072
      if [[ "$P4DRELNUM" > "2023.0" ]]; then
         if [[ -n "$(command -v setcap)" && ! -f /.dockerenv ]]; then
            SetcapCmd="setcap CAP_SYS_RESOURCE=+ep $SDPInstallRoot/common/bin/p4d_$P4DRELNUM.$P4DBLDNUM"
         fi
      fi
   fi
fi

#------------------------------------------------------------------------------
# Working in /p4/common/bin, create p4d sylinks if needed.
if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/common/bin" || bail "Could not do: cd $SDPInstallRoot/common/bin"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/common/bin"
fi

if [[ "$ServerType" == "p4d"* ]]; then
   if [[ ! -L "p4d_${SDPInstance}_bin" ]]; then
      run "ln -s p4d_${P4DRELNUM}_bin p4d_${SDPInstance}_bin" ||\
         errmsg "Failed to symlink p4d_${SDPInstance}_bin"
   fi
fi

#------------------------------------------------------------------------------
# Copy p4broker binary to version-tagged file and create p4broker links if
# p4broker binary exists.

if [[ -f "$TarRootBinDir/p4broker" ]]; then
   # shellcheck disable=SC2016
   P4BRELNUM=$("$TarRootBinDir/p4broker" -V | grep -i Rev. | awk -F / '{print $3}')
   # shellcheck disable=SC2016
   P4BBLDNUM=$("$TarRootBinDir/p4broker" -V | grep -i Rev. | awk -F / '{print $4}' | awk '{print $1}')
   if [[ ! -f "$SDPInstallRoot/common/bin/p4broker_$P4BRELNUM.$P4BBLDNUM" ]]; then
      if [[ "$UsePackageDirs" -eq 1 ]]; then
         run "cp $SDPPackageBins/p4broker $SDPInstallRoot/common/bin/p4broker_$P4BRELNUM.$P4BBLDNUM" ||\
            errmsg "Could not copy p4broker binary."
      else
         run "cp $TarRootBinDir/p4broker $SDPInstallRoot/common/bin/p4broker_$P4BRELNUM.$P4BBLDNUM" ||\
            errmsg "Could not copy p4broker binary."
      fi
   fi

   #------------------------------------------------------------------------------
   # Working in /p4/common/bin, create p4broker* sylinks if needed.
   if [[ "$NoOp" -eq 0 ]]; then
      cd "$SDPInstallRoot/common/bin" || bail "Could not do: cd $SDPInstallRoot/common/bin"
      msg "Operating in: $PWD"
   else
      msg "NO_OP: Would be operating in: $SDPInstallRoot/common/bin"
   fi

   if [[ -L "p4broker_${P4BRELNUM}_bin" ]]; then
      run "unlink p4broker_${P4BRELNUM}_bin" ||\
         errmsg "Could not unlink: p4broker_${P4BRELNUM}_bin"
   fi

   run "ln -s p4broker_$P4BRELNUM.$P4BBLDNUM p4broker_${P4BRELNUM}_bin" "Creating broker symlink." ||\
      errmsg "Could not create broker symlink."

   if [[ -L "p4broker_${SDPInstance}_bin" ]]; then
      run "unlink p4broker_${SDPInstance}_bin" ||\
         errmsg "Could not unlink: p4broker_${SDPInstance}_bin"
   fi

   run "ln -s p4broker_${P4BRELNUM}_bin p4broker_${SDPInstance}_bin" "Creating broker instance symlink." ||\
      errmsg "Could not create broker instance symlink."
fi

if [[ "$ServerType" == "p4broker" || "$InstallP4Broker" -eq 1 ]]; then
   if [[ "$NoOp" -eq 0 ]]; then
      cd "$SDPInstallRoot/$SDPInstance/bin" || \
         bail "Could not do: cd $SDPInstallRoot/$SDPInstance/bin"
      msg "Operating in: $PWD"
   else
      msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance/bin"
   fi

   if [[ ! -L "p4broker_${SDPInstance}" ]]; then
      run "ln -s $SDPInstallRoot/common/bin/p4broker_${SDPInstance}_bin p4broker_${SDPInstance}" ||\
         errmsg "Could not create symlink."
   fi

   msg "Generating Helix Broker init script: p4broker_${SDPInstance}_init"
   if [[ "$NoOp" -eq 0 ]]; then
      sed "s|REPL_SDP_INSTANCE|${SDPInstance}|g" "$TarRootInit/p4broker_instance_init.template" > "p4broker_${SDPInstance}_init"
   else
      msg "NO_OP: Would have generated p4broker_${SDPInstance}_init"
   fi
   run "chmod +x p4broker_${SDPInstance}_init"
fi

#------------------------------------------------------------------------------
# Copy p4p binary to version-tagged binary and create p4p symlinks if p4p
# binary exists.
if [[ -f "$TarRootBinDir/p4p" ]]; then
   # shellcheck disable=SC2016
   P4PRELNUM=$("$TarRootBinDir/p4p" -V | grep -i Rev. | awk -F / '{print $3}')
   # shellcheck disable=SC2016
   P4PBLDNUM=$("$TarRootBinDir/p4p" -V | grep -i Rev. | awk -F / '{print $4}' | awk '{print $1}')

   if [[ ! -f "$SDPInstallRoot/common/bin/p4p_$P4PRELNUM.$P4PBLDNUM" ]]; then
      if [[ "$UsePackageDirs" -eq 1 ]]; then
         run "cp $SDPPackageBins/p4p $SDPInstallRoot/common/bin/p4p_$P4PRELNUM.$P4PBLDNUM" ||\
            errmsg "Could not copy p4p binary."
      else
         run "cp $TarRootBinDir/p4p $SDPInstallRoot/common/bin/p4p_$P4PRELNUM.$P4PBLDNUM" ||\
            errmsg "Could not copy p4p binary."
      fi
   fi

   #------------------------------------------------------------------------------
   # Working in /p4/common/bin, create p4p* sylinks if needed.
   if [[ "$NoOp" -eq 0 ]]; then
      cd "$SDPInstallRoot/common/bin" || bail "Could not do: cd $SDPInstallRoot/common/bin"
      msg "Operating in: $PWD"
   else
      msg "NO_OP: Would be operating in: $SDPInstallRoot/common/bin"
   fi

   if [[ -L "p4p_${P4PRELNUM}_bin" ]]; then
      run "unlink p4p_${P4PRELNUM}_bin" || errmsg "Could not unlink: p4p_${P4PRELNUM}_bin"
   fi

   run "ln -s p4p_$P4PRELNUM.$P4PBLDNUM p4p_${P4PRELNUM}_bin" ||\
      errmsg "Could not symlink to p4p."

   if [[ -L "p4p_${SDPInstance}_bin" ]]; then
      run "unlink p4p_${SDPInstance}_bin" || errmsg "Could not unlink: p4p_${SDPInstance}_bin"
   fi

   run "ln -s p4p_${P4PRELNUM}_bin p4p_${SDPInstance}_bin"
fi

if [[ "$ServerType" == "p4p"* || "$InstallP4Proxy" -eq 1 ]]; then
   if [[ "$NoOp" -eq 0 ]]; then
      cd "$SDPInstallRoot/$SDPInstance/bin" || \
         bail "Could not do: cd $SDPInstallRoot/$SDPInstance/bin"
      msg "Operating in: $PWD"
   else
      msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance/bin"
   fi

   if [[ ! -L "p4p_${SDPInstance}" ]]; then
      run "ln -s $SDPInstallRoot/common/bin/p4p_${SDPInstance}_bin p4p_${SDPInstance}" ||\
         errmsg "Could not symlink for p4p_${SDPInstance}."
   fi

   msg "Generating Helix Proxy init script: p4p_${SDPInstance}_init"
   if [[ "$NoOp" -eq 0 ]]; then
      sed -e "s|REPL_SDP_INSTANCE|${SDPInstance}|g" \
         "$TarRootInit/p4p_instance_init.template" > "p4p_${SDPInstance}_init"
   else
      msg "NO_OP: Would have generated p4p_${SDPInstance}_init"
   fi

   run "chmod +x p4p_${SDPInstance}_init"

   if [[ "$NoOp" -eq 0 ]]; then
      cd "$SDPInstallRoot/$SDPInstance" || \
         bail "Could not do: cd $SDPInstallRoot/$SDPInstance"
   else
      msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance"
   fi

   run "ln -s /$DD/p4/$SDPInstance/cache"
fi

#------------------------------------------------------------------------------
# Generate SDP_ADMIN_PASSWORD_FILE file.
if [[ ! -e "$SDPInstallRoot/common/config/.p4passwd.${P4SERVER}.admin" ]]; then
   if [[ "$NoOp" -eq 0 ]]; then
      echo "$P4ADMINPASS" | base64 -d - > "$SDPInstallRoot/common/config/.p4passwd.${P4SERVER}.admin"
      [[ -s "$SDPInstallRoot/common/config/.p4passwd.${P4SERVER}.admin" ]] ||\
         errmsg "Failed to decode P4ADMINPASS from base64 form: [$P4ADMINPASS]."
   else
      msg "NO_OP: Would write admin password into $SDPInstallRoot/common/config/.p4passwd.${P4SERVER}.admin"
   fi
else
   warnmsg "Skipping update of existing admin password file $SDPInstallRoot/common/config/.p4passwd.${P4SERVER}.admin"
fi

#------------------------------------------------------------------------------
# Operating in the Instance Bin dir, /p4/N/bin
if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/$SDPInstance/bin" ||\
      bail "Could not do: cd $SDPInstallRoot/$SDPInstance/bin" 
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance/bin"
fi

if [[ ! -L "p4_${SDPInstance}" ]]; then
   run "ln -s $SDPInstallRoot/common/bin/p4_bin p4_${SDPInstance}" ||\
      errmsg "Could not create symlink for: p4_${SDPInstance}"
fi

if [[ "$InstallP4D" -eq 1 ]]; then
   msg "Generating Helix Core Server init script: p4d_${SDPInstance}_init"
   if [[ "$NoOp" -eq 0 ]]; then
      sed "s|REPL_SDP_INSTANCE|${SDPInstance}|g" \
         "$TarRootInit/p4d_instance_init.template" > "p4d_${SDPInstance}_init" ||\
         errmsg "Failed to generate init script."
      run "chmod +x p4d_${SDPInstance}_init" ||\
         errmsg "Failed to chmod init script."
   fi
fi

if [[ "$InstallP4DTG" -eq 1 ]]; then
   msg "Generating P4DTG init script: p4dtg_${SDPInstance}_init"
   if [[ "$NoOp" -eq 0 ]]; then
      sed "s|REPL_SDP_INSTANCE|${SDPInstance}|g" \
         "$TarRootInit/p4dtg_instance_init.template" > "p4dtg_${SDPInstance}_init" ||\
         errmsg "Failed to generate init script."
      run "chmod +x p4dtg_${SDPInstance}_init" ||\
         errmsg "Failed to chmod init script."
   fi
   warnmsg "The p4dtg_${SDPInstance}_init script has been generated. Additional steps are\\nrequired to complete installation."
fi

if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/$SDPInstance/bin" || bail "Could not do: cd $SDPInstallRoot/$SDPInstance/bin"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/$SDPInstance/bin"
fi

if [[ "$InstallP4D" -eq 1 ]]; then
   if [[ $CASE_SENSITIVE -eq 1 ]]; then
      if [[ ! -L "p4d_$SDPInstance" ]]; then
         msg "Generating P4D symlink for case-sensitive instance: p4d_${SDPInstance}."
         run "ln -s $SDPInstallRoot/common/bin/p4d_${SDPInstance}_bin p4d_$SDPInstance" ||\
            errmsg "Failed to symlink p4d_$SDPInstance"
      fi
   else
      msg "Generating P4D wrapper for case-insensitive instance: p4d_${SDPInstance}."
      if [[ "$NoOp" -eq 0 && ! -e "p4d_${SDPInstance}" ]]; then
         echo '#!/bin/bash' > "p4d_${SDPInstance}" || errmsg "Failed to generate script."
         echo "P4D=/p4/common/bin/p4d_${SDPInstance}_bin" >> "p4d_$SDPInstance" ||\
            errmsg "Failed to generate script."
         # shellcheck disable=SC2016
         echo 'exec $P4D -C1 "$@"' >> "p4d_$SDPInstance" ||\
            errmsg "Failed to generate script."
         run "chmod +x p4d_$SDPInstance" || errmsg "Failed to chmod wrapper script."
      fi
   fi
fi

if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/common/config" ||\
      bail "Could not do: cd $SDPInstallRoot/common/config"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/common/config"
fi

if [[ "$ServerType" == "p4p"* ]]; then
   P4P_TARGET_PORT="${SSL_PREFIX}${P4MASTERHOST}:${P4_PORT}"
   [[ "$TargetPortSet" -eq 1 ]] && P4P_TARGET_PORT="$TargetPort"
   [[ "$ListenPortSet" -eq 1 ]] && P4_PORT="$ListenPort"
fi

if [[ "$ServerType" == "p4broker" ]]; then
   P4_PORT="${SSL_PREFIX}${P4MASTERHOST}:${P4_PORT}"
   [[ "$TargetPortSet" -eq 1 ]] && P4_PORT="$TargetPort"
   [[ "$ListenPortSet" -eq 1 ]] && P4BROKER_PORT="$ListenPort"
fi

msg "Generating Instance Vars file: p4_${SDPInstance}.vars"
if [[ "$NoOp" -eq 0 ]]; then
   sed -e "s|REPL_MAILTO|${MAILTO}|g" \
      -e "s|REPL_MAILFROM|${MAILFROM}|g" \
      -e "s|REPL_ADMINUSER|${ADMINUSER}|g" \
      -e "s|REPL_MASTER_ID|${MASTER_ID}|g" \
      -e "s|REPL_SSLPREFIX|${SSL_PREFIX}|g" \
      -e "s|REPL_P4PORT|${P4_PORT}|g" \
      -e "s|REPL_P4BROKERPORT|${P4BROKER_PORT}|g" \
      -e "s|REPL_P4WEBPORT|${P4WEB_PORT}|g" \
      -e "s|REPL_P4FTPPORT|${P4FTP_PORT}|g" \
      -e "s|REPL_P4MASTERHOST|${P4MASTERHOST}|g" \
      -e "s|REPL_P4P_TARGET_PORT|${P4P_TARGET_PORT}|g" \
      "$TarRootCommon/config/instance_vars.template" > "p4_${SDPInstance}.vars" ||\
      errmsg "Failed to generate p4_${SDPInstance}.vars"
fi

if [[ "$ServerType" == p4_master ]]; then
   msg "Generating config file for P4Review: p4_${SDPInstance}.p4review.cfg"
   if [[ "$NoOp" -eq 0 ]]; then
      sed -e "s|REPL_ADMINISTRATOR|${MAILTO}|g" \
         -e "s|REPL_COMPLAINFROM|${COMPLAINFROM}|g" \
         -e "s|REPL_MAILHOST|${MAILHOST}|g" \
         -e "s|REPL_P4MASTERHOST|${P4MASTERHOST}|g" \
         "$TarRootCommon/config/p4review.cfg.template" > "p4_${SDPInstance}.p4review.cfg" ||\
         errmsg "Failed to generate p4_${SDPInstance}.p4review.cfg"
   fi
fi

# If the site has neither an actual or sample SiteTags.cfg file, install the sample.
if [[ -e "$SDPInstallRoot/common/config/SiteTags.cfg" ]]; then
   msg "Verified: SiteTags.cfg exists as $SDPInstallRoot/common/config/SiteTags.cfg."
elif [[ -e "$SDPInstallRoot/common/config/SiteTags.cfg.sample" ]]; then
   msg "Verified: SiteTags.cfg.sample exists as $SDPInstallRoot/common/config/SiteTags.cfg.sample"
else
   msg "Copying SiteTags.cfg.sample to $SDPInstallRoot/common/config/SiteTags.cfg.sample."
   if [[ "$NoOp" -eq 0 ]]; then
      cp -p "$TarRootConfig/SiteTags.cfg.sample" "$SDPInstallRoot/common/config/SiteTags.cfg.sample" ||\
      errmsg "Could not do: cp -p \"$TarRootConfig/SiteTags.cfg.sample\" \"$SDPInstallRoot/common/config/SiteTags.cfg.sample\""
   else
      msg "NO_OP: Would have done: cp -p \"$TarRootConfig/SiteTags.cfg.sample\" \"$SDPInstallRoot/common/config/SiteTags.cfg.sample\""
   fi
fi

if [[ -e "$SDPInstallRoot/common/config/configurables.cfg" ]]; then
   msg "Verified: configurables.cfg exists as $SDPInstallRoot/common/config/configurables.cfg."
else
   msg "Copying configurables.cfg to $SDPInstallRoot/common/config/configurables.cfg"
   if [[ "$NoOp" -eq 0 ]]; then
      cp -p "$TarRootConfig/configurables.cfg" "$SDPInstallRoot/common/config/configurables.cfg" ||\
      errmsg "Could not do: cp -p \"$TarRootConfig/configurables.cfg\" \"$SDPInstallRoot/common/config/configurables.cfg\""
   else
      msg "NO_OP: Would have done: cp -p \"$TarRootConfig/configurables.cfg\" \"$SDPInstallRoot/common/config/configurables.cfg\""
   fi
fi

if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot" || bail "Could not do: cd $SDPInstallRoot"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot"
fi

# Generate crontab for p4d servers, standalone proxies, and standalone brokers.
SDPCrontabDir="$SDPInstallRoot/common/etc/cron.d"
if [[ "$ServerType" == "p4d"* || "$ServerType" == "p4p"* || "$ServerType" == "p4broker" ]]; then
   if [[ ! -f "${SDPInstallRoot}/p4.crontab.${SDPInstance}" ]]; then
      CrontabFileInP4="p4.crontab.${SDPInstance}"
   else
      CrontabFileInP4="p4.crontab.$SDPInstance.new"
      run "rm -f ${SDPInstallRoot}/$CrontabFileInP4"
      msg "Existing crontab file found; writing new crontab to ${SDPInstallRoot}/${CrontabFileInP4}"
   fi

   CrontabFile="$SDPCrontabDir/crontab.$OSUSER.$ThisHost.${SDPInstance}"

   if [[ "$ServerType" == "p4d"* ]]; then
      msg "Generating p4d server crontab: $CrontabFileInP4"
      if sed -e "s|REPL_MAILTO|${MAILTO}|g" \
         -e "s|REPL_MAILFROM|${MAILFROM}|g" \
         -e "s|REPL_INSTANCE|${SDPInstance}|g" \
         "$TarRootCron/template.crontab.combined" > "$TmpFile"; then
         CrontabFileGenerated=1
      else
         errmsg "Failed to generate crontab file from: $TarRootCron/template.crontab.combined"
      fi
   elif [[ "$ServerType" == "p4p"* ]]; then
      msg "Generating p4p server crontab: $CrontabFileInP4"
      if sed -e "s|REPL_MAILTO|${MAILTO}|g" \
         -e "s|REPL_MAILFROM|${MAILFROM}|g" \
         -e "s|REPL_INSTANCE|${SDPInstance}|g" \
         "$TarRootCron/template.crontab.proxy" > "$TmpFile"; then
         CrontabFileGenerated=1
      else
         errmsg "Failed to generate crontab file from $TarRootCron/template.crontab.proxy"
      fi
   elif [[ "$ServerType" == "p4broker" ]]; then
      msg "Generating p4broker server crontab: $CrontabFileInP4"
      if sed -e "s|REPL_MAILTO|${MAILTO}|g" \
         -e "s|REPL_MAILFROM|${MAILFROM}|g" \
         -e "s|REPL_INSTANCE|${SDPInstance}|g" \
         "$TarRootCron/template.crontab.broker" > "$TmpFile"; then
         CrontabFileGenerated=1
      else
         errmsg "Failed to generate crontab file from $TarRootCron/template.crontab.broker"
      fi
   fi

   if [[ "$NoOp" -eq 0 ]]; then
      msg "Deploying generated crontab file: $CrontabFileInP4"
      mv -f "$TmpFile" "$CrontabFileInP4" ||\
         errmsg "Failed to do: mv -f $TmpFile $CrontabFileInP4"
      cp -f "$CrontabFileInP4" "$CrontabFile" ||\
         errmsg "Failed to do: cp -f $CrontabFileInP4 $CrontabFile"
   else
      msg "NO_OP: Would have generated crontab $CrontabFileInP4 with contents:"
      cat "$TmpFile"
      rm -f "$TmpFile"
   fi

   #------------------------------------------------------------------------------
   # Load crontab (unless '-no_cron' or '-test' were specified).
   if [[ "$LoadActiveCrontab" -eq 1 ]]; then
      if [[ "$CrontabFileGenerated" -eq 1 ]]; then
         if [[ -r "$CrontabFile" ]]; then
            if [[ "$RunningAsRoot" -eq 1 ]]; then
               if [[ -r /etc/cron.allow ]]; then
                  if [[ "$NoOp" -eq 0 ]]; then
                     echo "$OSUSER" >> /etc/cron.allow ||\
                        errmsg "Could not do: echo \"$OSUSER\" >> /etc/cron.allow"
                  else
                     msg "NO_OP: Would do: echo \"$OSUSER\" >> /etc/cron.allow"
                  fi
               fi

               if [[ "$TestMode" -eq 0 ]]; then
                  if [[ "$(crontab -u "$OSUSER" -l 2>&1)" == "no crontab for $OSUSER" ]]; then
                     run "crontab -u $OSUSER $CrontabFile" "As root, loading crontab for $OSUSER." ||\
                        errmsg "As root, failed to load crontab for user $OSUSER."
                  else
                     warnmsg "User $OSUSER already has a crontab, so new crontab entries have not been applied. Consider updating crontab for $OSUSER to include new entries in $CrontabFile."
                  fi
               else
                  msg "If '-test' were not used, would have loaded crontab for $OSUSER as root with: crontab -u $OSUSER $CrontabFile" 
               fi
            else
               if [[ "$TestMode" -eq 0 ]]; then
                  if [[ "$(crontab -l 2>&1)" == "no crontab for $OSUSER" ]]; then
                     run "crontab $CrontabFile" "As $OSUSER, loading crontab." ||\
                        errmsg "As $OSUSER, failed to load crontab."
                  else
                     warnmsg "User $OSUSER already has a crontab, so new crontab entries have not been applied. Consider updating crontab to include new entries in $CrontabFile."
                  fi
               else
                  msg "If '-test' were not used, would have loaded crontab with: crontab $CrontabFile"
               fi
            fi
         else
            errmsg "Not loading crontab for user $OSUSER; did not find expected crontab file: $CrontabFile"
         fi
      else
         warnmsg "Not loading crontab for user $OSUSER because crontab was not generated."
      fi
   else
      msg "Not loading crontab due to '-no_cron'."
   fi

   if [[ "$DoChownCommands" -eq 1 && -e "$CrontabFileInP4" ]]; then
      run "chown $OSUSER:$OSGROUP $CrontabFileInP4 $CrontabFile" || \
         errmsg "Failed to chown $CrontabFileInP4 $CrontabFile"
   fi
fi

if [[ -r "$TarRootUnsupported/Maintenance/template.maintenance.cfg" && "$ServerType" == "p4d"* ]]; then
   msg "Appending configuration section [$SDPInstance] to $TarRootUnsupported/Maintenance/maintenance.cfg"

   if [[ -r "$TarRootUnsupported/Maintenance/maintenance.cfg" ]]; then
      run "chmod +w $TarRootUnsupported/Maintenance/maintenance.cfg"
   fi

   if [[ "$NoOp" -eq 0 ]]; then
      echo -e "[$SDPInstance]" >> "$TarRootUnsupported/Maintenance/maintenance.cfg"

      msg "Appending to: $TarRootUnsupported/Maintenance/maintenance.cfg"
      sed -e "s|REPL_INSTANCE|$SDPInstance|g" \
         -e "s|REPL_ADMINISTRATOR|${MAILTO}|g" \
         -e "s|REPL_COMPLAINFROM|${COMPLAINFROM}|g" \
         -e "s|REPL_MAILHOST|${MAILHOST}|g" \
         -e "s|REPL_DOMAIN|${COMPLAINFROM_DOMAIN}|g" \
         "$TarRootUnsupported/Maintenance/template.maintenance.cfg" >> "$TarRootUnsupported/Maintenance/maintenance.cfg" ||\
         errmsg "Could not append to: $TarRootUnsupported/Maintenance/maintenance.cfg"
   fi
fi

if [[ "$NoOp" -eq 0 ]]; then
   cd "$SDPInstallRoot/${SDPInstance}/bin" || \
      bail "Could not do: cd $SDPInstallRoot/${SDPInstance}/bin"
   msg "Operating in: $PWD"
else
   msg "NO_OP: Would be operating in: $SDPInstallRoot/${SDPInstance}/bin"
fi

if [[ "$SHAREDDATA" == "TRUE" ]]; then
   if [[ "$ServerType" == p4d_replica || "$ServerType" == p4d_standby ]]; then
      msg "Configuring Replica sharing depot data with master. Skipping chown of /$DD and /$DD/p4"
      DoNFSChownCommands=0
   fi
fi

if [[ "$DoChownCommands" -eq 1 ]]; then
   if [[ "$TestMode" -eq 0 ]]; then
      if [[ "$DoNFSChownCommands" -eq 1 ]]; then
         run "chown $OSUSER:$OSGROUP /$DD" || errmsg "Failed to chown /$DD"
      fi

      run "chown $OSUSER:$OSGROUP /$LG" || errmsg "Failed to chown /$LG"

      if [[ "$ServerType" == "p4d"* ]]; then
         run "chown $OSUSER:$OSGROUP /$DB1" || errmsg "Failed to chown /$DB1"

         if [[ "$DB1" != "$DB2" ]]; then
            run "chown $OSUSER:$OSGROUP /$DB2" || errmsg "Failed to chown /$DB2"
         fi
      fi
   fi

   if [[ "$DoNFSChownCommands" -eq 1 ]]; then
      run "chown $OSUSER:$OSGROUP /$DD/p4" || errmsg "Failed to chown /$DD/p4"
      if [[ "$CD" != "$DD" ]]; then
         run "chown $OSUSER:$OSGROUP /$CD/p4" || errmsg "Failed to chown /$CD/p4"
      fi
   fi

   run "chown $OSUSER:$OSGROUP /$LG/p4" || errmsg "Failed to chown /$LG/p4"

   if [[ "$ServerType" == "p4d"* ]]; then
      run "chown $OSUSER:$OSGROUP /$DB1/p4" || errmsg "Failed to chown  /$DB1/p4"

      if [[ "$DB1" != "$DB2" ]]; then
         run "chown $OSUSER:$OSGROUP /$DB2/p4" || errmsg "Failed to chown  /$DB2/p4"
      fi
   fi

   run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot" || errmsg "Failed to chown $SDPInstallRoot"
   run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot/$SDPInstance" ||\
      errmsg "Failed to chown $SDPInstance"
   run "chown -h $OSUSER:$OSGROUP $SDPInstallRoot/common" ||\
      errmsg "Failed to chown $SDPInstallRoot/common"

   for path in "$SDPInstallRoot/sdp" "$SDPInstallRoot"/* "$SDPInstallRoot/$SDPInstance"/*; do
      run "chown -h $OSUSER:$OSGROUP $path" || errmsg "Failed to chown -h $OSUSER:$OSGROUP $path"
   done

   run "chown -Rh $OSUSER:$OSGROUP $SDPInstallRoot/common/" || errmsg "Failed to chown $SDPInstallRoot/common/"
   if [[ $TestMode -eq 0 ]]; then
      run "chown -Rh $OSUSER:$OSGROUP $SDPInstallRoot/sdp/" || errmsg "Failed to chown $SDPInstallRoot/sdp/"
   fi
   run "chown -Rh $OSUSER:$OSGROUP $SDPInstallRoot/common" || errmsg "Failed to chown $SDPInstallRoot/common"
   if [[ "$ServerType" == "p4d"* ]]; then
      run "chown -Rh $OSUSER:$OSGROUP /$DB1/p4/$SDPInstance" || errmsg "Failed to chown /$DB1/p4/$SDPInstance"
      if [[ "$DB1" != "$DB2" ]]; then
         run "chown -Rh $OSUSER:$OSGROUP /$DB2/p4/$SDPInstance" || errmsg "Failed to chown /$DB2/p4/$SDPInstance"
      fi
   fi
   run "chown -Rh $OSUSER:$OSGROUP /$LG/p4/$SDPInstance" || errmsg "Failed to chown /$LG/p4/$SDPInstance"

   run "chown -Rh $OSUSER:$OSGROUP $SDPInstallRoot/$SDPInstance/bin" || errmsg "Failed to chown $SDPInstallRoot/$SDPInstance/bin"
else
   msg "Skipped chown/chmod commands for large directory trees."
fi

if [[ "$ServerType" == "p4d"* ]]; then
   run "chmod $ExecPerms /$DB1/p4" || errmsg "Failed to chmod /$DB1/p4"
   if [[ "$DB1" != "$DB2" ]]; then
      run "chmod $ExecPerms /$DB2/p4" || errmsg "Failed to chmod /$DB2/p4"
   fi
fi

run "chmod $ExecPerms /$DD/p4" || errmsg "Failed to chmod /$DD/p4"
if [[ "$CD" != "$DD" ]]; then
   run "chmod $ExecPerms /$CD/p4" || errmsg "Failed to chmod /$CD/p4"
fi
run "chmod $ExecPerms /$LG/p4" || errmsg "Failed to chmod /$LG/p4"

if [[ "$ServerType" == "p4d"* ]]; then
   run "chmod -R $ExecPerms /$DB1/p4/$SDPInstance" || errmsg "Failed to chmod /$DB1/p4/$SDPInstance"
   if [[ "$DB1" != "$DB2" ]]; then
      run "chmod -R $ExecPerms /$DB2/p4/$SDPInstance" || errmsg "Failed to chmod /$DB2/p4/$SDPInstance"
   fi
fi

run "chmod -R $ExecPerms $SDPInstallRoot/common" || errmsg "Failed to chmod $SDPInstallRoot/common"
run "chmod -R $ExecPerms /$LG/p4/$SDPInstance" || errmsg "Failed to chmod /$LG/p4/$SDPInstance"

if [[ "$InstallP4D" -eq 1 ]]; then
   if [[ "$SDPInstance" != "$MASTERINSTANCE" ]]; then
      if [[ -f "$SDPInstallRoot/$MASTERINSTANCE/root/license" && ! -f "$SDPInstallRoot/$SDPInstance/root/license" ]]; then
         run "cp -p $SDPInstallRoot/$MASTERINSTANCE/root/license $SDPInstallRoot/$SDPInstance/root/license" ||\
            errmsg "Failed to copy license from master instance $MASTERINSTANCE to $SDPInstance."
         run "chown $OSUSER:$OSGROUP $SDPInstallRoot/$SDPInstance/root/license" ||\
            errmsg "Failed to chown $SDPInstallRoot/$SDPInstance/root/license"
      fi
   fi
fi

for f in "$SDPInstallRoot/${SDPInstance}"/bin/*_init; do
   run "chmod 755 $f" || errmsg "Failed to chmod $f to 755."
done

run "chmod $ExecPerms $SDPInstallRoot/ssl" || errmsg "Failed to chmod $ExecPerms $SDPInstallRoot/ssl"

for f in "$SDPInstallRoot/common/config/.p4passwd.${P4SERVER}."* "$SDPInstallRoot"/ssl/*; do
   run "chmod $ReadPerms $f" || errmsg "Failed to chmod $f to $ReadPerms."
done

if [[ "$DoChownCommands" -eq 1 ]]; then
   run "chown -R $OSUSER:$OSGROUP $SDPInstallRoot/ssl" ||\
      errmsg "Failed to do chown for ssl dir."
   if [[ "$UsePackageDirs" -eq 1 ]]; then
      DirList="downloads helix_binaries p4"

      for d in $DirList; do
         if [[ ! -d "$d" ]]; then
            run "chown -R $OSUSER:$OSGROUP $SDPPackageBase/$d" ||\
               errmsg "Failed to chown dir $SDPPackageBase/$d"
         fi
      done
   fi
fi

if [[ "$TestMode" -eq 1 ]]; then
   msg "\\nThis was done in test mode. Production install directories were not affected, and systemd/SELinux config were not attempted.\\n"
  
   if [[ -d "/p4/common" ]]; then
      msg "If you are upgrading an existing SDP installation, compare these current folders and files:

diff -qr /p4/$SDPInstance/bin $SDPInstallRoot/$SDPInstance/bin
diff -qr /p4/common $SDPInstallRoot/common

Be careful to ensure generated files are correct, including files in
/p4/common/config and /p4/common/bin/p4_vars are appropriate.\\n"
   fi
fi

#------------------------------------------------------------------------------
# Enable the OOM Killder defense feature.
if [[ -n "$SetcapCmd" ]]; then
   if [[ "$RunningAsRoot" -eq 1 ]]; then
      msg "Enabling OOM killer protection for p4d.\\nRunning: $SetcapCmd"
      # shellcheck disable=SC2086
      if eval $SetcapCmd; then
         run "getcap $SDPInstallRoot/common/bin/p4d_$P4DRELNUM.$P4DBLDNUM"
      else
         errmsg "Failed to do: $SetcapCmd"
      fi
   else
      warnmsg "Additional Processing is advised to activate OOM killer protection for p4d:\\n"
      msg "As root, execute this command:\\n\\t$SetcapCmd\\n"
      msg "And then restart the p4d_${SDPInstance} service."
   fi
fi

#------------------------------------------------------------------------------
# Determine Init Mechanism
if [[ -d "/etc/systemd/system" ]]; then
   InitMechanism="Systemd"

   if [[ -z "$(command -v systemctl)" ]]; then
      warnmsg "systemctl isn't in PATH, but /etc/systemd/system exists. Acting as if '-no_systemd' was specified."
      UseSystemd=0
   fi
elif [[ -x "/sbin/launchd" ]]; then
   InitMechanism="Launchd"
   UseSystemd=0
elif [[ -d "/etc/init.d" ]]; then
   InitMechanism="SysV"
   UseSystemd=0
else
   bail "Could not determine init mechanism. Systemd, SysV, and Launchd aren't available. Aborting."
fi

SystemdTemplatesDir="$SDPInstallRoot/common/etc/systemd/system"
if [[ "$RunningAsRoot" -eq 1 && "$DoServiceInit" -eq 1 ]]; then
   # Determine list of binaries to install.
   [[ "$InstallP4D" -eq 1 ]] && BinList+=" p4d"
   [[ "$InstallP4Broker" -eq 1 ]] && BinList+=" p4broker"
   [[ "$InstallP4Proxy" -eq 1 ]] && BinList+=" p4p"

   if [[ "$InitMechanism" == "SysV" ]]; then
      if [[ -n "$(command -v chkconfig)" ]]; then
         msg "\\nConfiguring $InitMechanism services.\\n"
         cd /etc/init.d || bail "Could not cd to [/etc/init.d]."
         for svc in $BinList; do
            initScript=${svc}_${SDPInstance}_init
            if [[ -x /p4/${SDPInstance}/bin/$initScript ]]; then
               run "ln -s /p4/${SDPInstance}/bin/$initScript"
               run "chkconfig --add $initScript"
               run "chkconfig $initScript on"
            fi
         done
      else
         msg "Skipping SysV service initiailzation; no 'chkconfig' in PATH."
      fi
   elif [[ "$InitMechanism" == "Systemd" && "$UseSystemd" -eq 1 ]]; then
      msg "\\nConfiguring $InitMechanism services.\\n"
      cd /etc/systemd/system || bail "Could not cd to /etc/systemd/system."
      for binary in $BinList; do
         svcName="${binary}_${SDPInstance}"
         svcFile="${svcName}.service"

         # If the version of SDP deployed is 2020.1+, it will have templates
         # for systemd unit files. Use those if found, otherwise use the
         # baseline templates that come with the Helix Installer.
         svcTemplate="$SystemdTemplatesDir/${binary}_N.service.t"

         sed -e "s|__INSTANCE__|${SDPInstance}|g" \
            -e "s|__OSUSER__|$OSUSER|g" \
            "$svcTemplate" > "$svcFile" ||\
            bail "Failed to generate $PWD/$svcFile from template $svcTemplate."

         if [[ "$EnableSystemdServices" -eq 1 ]]; then
            run "systemctl enable $svcName" "Enabling $svcName to start on boot." ||\
               warnmsg "Failed to enable $svcName with $InitMechanism."
         else
            msg "Not enabling service $svcName due to '-no_enable'."
         fi

         SELinuxMode="$(getenforce 2>/dev/null || sestatus 2>/dev/null | grep -i 'Current mode' | awk '{print $NF}')"
         if [[ "$SELinuxMode" =~ ^(Enforcing|Permissive)$ ]]; then
            if [[ -n "$(command -v semanage)" ]]; then
               if run "semanage fcontext -a -t bin_t /p4/${SDPInstance}/bin/${binary}_${SDPInstance}_init" \
                  "Configuring SELinux for init script /p4/${SDPInstance}/bin/${binary}_${SDPInstance}_init." > "$TmpFile" 2>&1; then
                  cat "$TmpFile"
                  msg "SELinux configured OK."
               else
                  # If we get an error indicating semanage is already defined, suppress that.
                  if grep -q -E "ValueError: File context for .* already defined" "$TmpFile"; then
                     msg "SELinux configured OK (it was already defined)."
                  else
                     cat "$TmpFile"
                     errmsg "Failed SELinux addition of init script for /p4/${SDPInstance}/bin/${binary}_${SDPInstance}_init."
                  fi
               fi
               rm -f "$TmpFile"
            else
                warnmsg "SELinux is available but semanage not in PATH. Skipping semanage setup"
            fi

            if [[ -n "$(command -v restorecon)" ]]; then
               run "restorecon -vF /p4/${SDPInstance}/bin/${binary}_${SDPInstance}_init" ||\
                  errmsg "Failed SELinux restorecon of init script for ${binary}_${SDPInstance}."
            else
                warnmsg "SELinux is available but restorecon not in PATH. Skipping restorecon setup."
            fi
         else
            msg "SELinux is not available. Skipping Skipping semanage and restorecon setup."
         fi
      done

      run "systemctl daemon-reload" "Reloading systemd after generating Systemd unit files." ||\
         warnmsg "Failed to reload systemd daemon."
   fi
else
   if [[ "$DoServiceInit" -eq 0 ]]; then
      msg "Skipping $InitMechanism and SELinux configuration due to '-no_init'."
   else
      msg "Skipping $InitMechanism and SELinux configuration; not running as root."
   fi
fi

#------------------------------------------------------------------------------
# Configure Firewall.
if [[ "$RunningAsRoot" -eq 1 ]]; then
   if [[ "$DoFirewall" -eq 1 ]]; then
      if [[ -r "/etc/firewalld/services" ]]; then
         FirewallType="Firewalld"
         FirewallDir="/etc/firewalld/services"
      elif [[ -r "/etc/ufw/applications.d" ]]; then
         FirewallType="ufw"
         FirewallDir="/etc/ufw/applications.d"
      elif [[ -r "/etc/sysconfig/iptables" ]]; then
         FirewallType="IPTables"
         FirewallDir="/etc/sysconfig"
      else
         FirewallType="unknown firewall type or no firewall detected"
      fi

      # Verify firewall is available.
      if [[ "$FirewallType" == "Firewalld" ]]; then
         # FirewallOnline indicates that the firewall-cmd utility is available and the firewalld is online.
         if firewall-cmd --state > /dev/null 2>&1; then
            msg "Verified: firewalld is available and running."
            FirewallOnline=1
         else
            warnmsg "Skipping firewalld configuration because firwalld is not running."
         fi
      elif [[ "$FirewallType" == "ufw" ]]; then
         # FirewallOnline indicates that the ufw utility is available and the ufw firewall is online.
         if [[ "$(ufw status 2>/dev/null)" =~ ' active' ]]; then
            msg "Verified: ufw firewall is available and running."
            FirewallOnline=1
         else
            warnmsg "Skipping ufw firewall configuration because ufw is not running."
         fi
      else
         warnmsg "Skipping firewall configuration because firewall type is not handled: $FirewallType."
      fi
   else
      msg "Skipping firewall configuration due to '-no_firewall'."
   fi
else
   dbg "Skipping firewall configuration (not running as root)."
fi

#------------------------------------------------------------------------------
# Add Firewall rules.
if [[ "$FirewallOnline" -eq 1 ]]; then
   # Add Firewall rules for Firewalld, popular on the RHEL/Rocky Linux family.
   if [[ "$FirewallType" == "Firewalld" ]]; then
      msg "\\nConfiguring $FirewallType services.\\n"
      TmpFile=$(mktemp)

      for binary in $BinList; do
         svcName="${binary}_${SDPInstance}"
         svcFile="${svcName}.xml"
         if [[ "$NoOp" -eq 0 ]]; then
            sed -e "s|__INSTANCE__|$SDPInstance|g" \
               -e "s|__P4PORT__|${P4_PORT##*:}|g" \
               -e "s|__P4BROKER_PORT__|${P4BROKER_PORT##*:}|g" \
               "$WritableSDPDir/Server/Unix/p4/common/etc/firewalld/${binary}_N.xml.t" > "$TmpFile" ||\
               bail "Failed to generate firewalld service file."
         else
            msg "NO_OP: Would have generated firewalld entry using template: $WritableSDPDir/Server/Unix/p4/common/etc/firewalld/${binary}_N.xml.t"
         fi
         run "cp -f $TmpFile $FirewallDir/$svcFile" \
            "Installing generated firewalld service file to $FirewallDir/$svcFile." ||\
            bail "Failed to install $FirewallDir/$svcFile."
         run "firewall-cmd --add-service=$svcName" \
            "Adding firewall entry for $svcName." ||\
            warnmsg "Adding firewall entry for $svcName failed."
      done

      rm -f "$TmpFile"

      run "firewall-cmd --runtime-to-permanent" \
         "Permanently adding firewall entries" ||\
         warnmsg "Adding permanent firewall entries failed."

      run "firewall-cmd --reload" "Reloading firewalld." ||\
         warnmsg "Firewalld reload failed."

      if [[ -n "$(command -v iptables-save)" ]]; then
         run "iptables-save" "Showing firewall rules." ||\
            warnmsg "Showing firewall failed."
      fi

   # Add Firewall rules for ufw, popular on the Ubuntu family.
   elif [[ "$FirewallType" == "ufw" ]]; then
      msg "\\nConfiguring $FirewallType services.\\n"
      TmpFile=$(mktemp)

      for binary in $BinList; do
         svcName="${binary}_${SDPInstance}"
         if [[ "$NoOp" -eq 0 ]]; then
            sed -e "s|__INSTANCE__|$SDPInstance|g" \
               -e "s|__P4PORT__|${P4_PORT##*:}|g" \
               -e "s|__P4BROKER_PORT__|${P4BROKER_PORT##*:}|g" \
               "$WritableSDPDir/Server/Unix/p4/common/etc/ufw/${binary}_N.t" > "$TmpFile" ||\
               bail "Failed to generate ufw firewall application file."
         else
            msg "NO_OP: Would have generated ufw firewall rule using template: $WritableSDPDir/Server/Unix/p4/common/etc/ufw/${binary}_N.t"
         fi

         run "cp -f $TmpFile $FirewallDir/$svcName" \
            "Installing generated ufw firewall application file to $FirewallDir/$svcName." ||\
            bail "Failed to install $FirewallDir/$svcName."
         run "ufw app update $svcName" \
            "Updating ufw firewall application $svcName." ||\
            bail "Failed to update ufw firewall application $svcName."
         run "ufw allow $svcName" \
            "Adding ufw firewall rules for application $svcName." ||\
            bail "Failed to allow ufw firewall rules for application $svcName."
      done

      rm -f "$TmpFile"

      run "ufw reload " "Reloading ufw firewall." ||\
         warnmsg "ufw firewall reload failed."

      if [[ -n "$(command -v iptables-save)" ]]; then
         run "iptables-save" "Showing firewall rules." ||\
            warnmsg "Showing firewall failed."
      fi
   fi
fi

#------------------------------------------------------------------------------
# Do the big, long-running things at the end.

msg "\\nAll structural processing is complete. Wrapping up with potentially long-running chown/chmod tasks."

if [[ "$SHAREDDATA" == "FALSE" && -d "/$DD/p4/$SDPInstance" ]]; then
   if [[ "$FastMode" -eq 0 ]]; then
      msg "Setting permissions on depot files - this may take some time ..."
      run "chmod -R $ExecPerms /$DD/p4/$SDPInstance" || errmsg "Failed to chmod: /$DD/p4/$SDPInstance"
   else
      msg "Skipped chmod -R of /$DD/p4/$SDPInstance due to '-f'."
   fi
fi

if [[ "$CD" != "$DD" ]]; then
   if [[ "$SHAREDDATA" == "FALSE" && -d "/$CD/p4/$SDPInstance" ]]; then
      if [[ "$FastMode" -eq 0 ]]; then
         msg "Setting permissions on depot files - this may take some time ..."
         run "chmod -R $ExecPerms /$CD/p4/$SDPInstance" || errmsg "Failed to chmod: /$CD/p4/$SDPInstance"
      else
         msg "Skipped chmod -R of /$CD/p4/$SDPInstance due to '-f'."
      fi
   fi
fi

if [[ "$SHAREDDATA" == "FALSE" && -d "/$DD/p4/$SDPInstance" ]]; then
   if [[ "$FastMode" -eq 0 ]]; then
      msg "Setting ownership on depot files - this may take some time ..."
      run "chown -Rh $OSUSER:$OSGROUP /$DD/p4/$SDPInstance" || errmsg "Failed to chown: /$DD/p4/$SDPInstance"
   else
      msg "Due to '-f' (fast), skipped chown -Rh of /$DD/p4/$SDPInstance; doing targeted chowns instead."
      Cmd="chown -h $OSUSER:$OSGROUP /$DD/p4/$SDPInstance"
      [[ -d "/$DD/p4/$SDPInstance/depots" ]] && Cmd+=" /$DD/p4/$SDPInstance/depots"
      [[ -d "/$DD/p4/$SDPInstance/cache" ]] && Cmd+=" /$DD/p4/$SDPInstance/cache"
      run "$Cmd" "Doing targeted chown commands in /$DD:" || \
         errmsg "Failed to do: $Cmd"
   fi
fi

if [[ "$CD" != "$DD" ]]; then
   if [[ "$SHAREDDATA" == "FALSE" && -d "/$CD/p4/$SDPInstance" ]]; then
      if [[ "$FastMode" -eq 0 ]]; then
         Cmd="chown -Rh $OSUSER:$OSGROUP /$CD/p4/$SDPInstance"
         run "$Cmd" "Setting ownership on checkpoint files - this may take some time ..." ||\
            errmsg "Failed to do: $Cmd"
      else
         msg "Due to '-f' (fast), skipped chown -Rh of /$CD/p4/$SDPInstance; doing targeted chowns instead."
         if [[ -d "/$CD/p4/$SDPInstance/checkpoints" ]]; then
            Cmd="chown -h $OSUSER:$OSGROUP /$CD/p4/$SDPInstance/checkpoints*"
            run "$Cmd" "Doing targeted chown commands in /$CD:" || \
               errmsg "Failed to do: $Cmd"
         fi
      fi
   fi
else
   if [[ "$FastMode" -eq 1 ]]; then
      if [[ -d "/$DD/p4/$SDPInstance/checkpoints" ]]; then
         msg "Due to '-f' (fast), doing targeted chowns for checkpoint dirs."
         Cmd="chown -h $OSUSER:$OSGROUP /$DD/p4/$SDPInstance/checkpoints*"
         run "$Cmd" "Doing targeted chown commands for checkpoints in /$DD/p4/$SDPInstance/checkpoints:" || \
            errmsg "Failed to do: $Cmd"
      fi
   fi
fi

if [[ "$UpdateSudoers" -eq 1 ]]; then
   Cmd="$GenSudoersScript"

   if [[ "$LimitedSudoers" -eq 1 ]]; then
      Cmd+=" -limited"
   else
      Cmd+=" -full"
   fi

   if [[ "$TestMode" -eq 0 ]]; then
      Cmd+=" -y -f"
   fi

   if [[ -x "$GenSudoersScript" ]]; then
      if [[ "$NoOp" -eq 0 ]]; then
         msg "Running: $Cmd"
         $Cmd || errmsg "Error from script to update sudoers, $GenSudoersScript."
      else
         msg "NO_OP: Would run: $Cmd"
      fi
   else
      errmsg "Skipping sudoers update because this is not executable: $GenSudoersScript"
   fi
fi

exit "$ErrorCount"
