#!/bin/bash
set -u
#==============================================================================
# This script serves as a guide defining best-practice configurables for a
# production environment.  See documentation regarding configurables here:
# https://www.perforce.com/perforce/doc.current/manuals/cmdref/Content/CmdRef/configurables.configurables.html
#
# 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 P4PORT and P4USER and run p4 login before running this script.

declare SpecFile=
declare ProtectsFile=
declare ProtectsTemplate=
declare DiskSpaceAvail=
declare MinKBFor5GLimits=7340032
declare LOGFILE=
declare -i ErrorCount=0
declare -i WarningCount=0

function msg () { echo -e "$*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function warnmsg () { msg "\\nWarning: ${1:-Unknown Warning}\\n"; WarningCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "${2:-1}"; }

# Verify instance value
INSTANCE=${1:-}
if [[ -n "$INSTANCE" ]]; then
   # shellcheck disable=SC1091
   source /p4/common/bin/p4_vars "$INSTANCE" ||\
      bail "Failed to load SDP environment."

   # shellcheck disable=SC1091
   source /p4/common/bin/backup_functions.sh ||\
      bail "Failed to load backup_functions.sh."
else
    bail "An instance argument is required."
fi

LOGFILE="${LOGS:-/tmp}/configure_new_server.$(date +'%Y%m%d-%H%M').log"
check_vars
set_vars

touch "${LOGFILE}" || bail "Couldn't touch log file [${LOGFILE}]."

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

log "${0##*/} configuring $P4SERVER on $(date)."
msg "Logging to: $LOGFILE"
msg "See documentation regarding configurables here:\\n
https://www.perforce.com/perforce/doc.current/manuals/cmdref/Content/CmdRef/configurables.configurables.html\\n"

msg "Starting p4d service (if needed)."
start_p4d
sleep 1

if [[ "$P4PORT" =~ ^ssl[46]*: ]]; then
   msg "Trusting P4PORT [$P4PORT]."
   p4 trust -f -y > /dev/null 2>&1 || bail "Could not trust P4PORT [$P4PORT]. Aborting."
fi

# Generate the super user account, but only if there is only a single account
# on the server.
if [[ "$(p4 users|wc -l)" ]]; then
   SpecFile="$(mktemp)"
   if p4 --field User="$P4USER" --field FullName="Perforce Helix Admin" --field Email="$P4USER@${MAILFROM##*@}" user -o "$P4USER" > "$SpecFile"; then
      msg "Creating user '$P4USER'."
      if p4 -s user -f -i < "$SpecFile"; then
         yes "$(cat "$SDP_ADMIN_PASSWORD_FILE")" | p4 passwd
         "$P4CBIN"/p4login

	 # Verify the Protections table is not initialized so we don't overwrite an existing table.
	 # Check for any entries in the db.protect table.
	 if [[ -z "$("$P4DBIN" -r "$P4ROOT" -k db.protect -jd - | grep ^@pv@ | head -1)" ]]; then
	    msg "Initializing Protections table."
	    ProtectsFile=$(mktemp)
	    ProtectsTemplate="${0%/*}/protect.p4t"
	    if [[ -r "$ProtectsTemplate" ]]; then
	       if sed -e "s@__P4USER__@$P4USER@g" "$ProtectsTemplate" > "$ProtectsFile"; then
                  if p4 -s protect -i < "$ProtectsFile"; then
                     msg "Protections table initialized to:\\n$(p4 protect -o | grep -v '^#')\\n"
		  else
                     errmsg "Failed to load generated Protections file:\\n$(cat "$ProtectsFile")"
		  fi
	       else
                  errmsg "Failed to generate Protections file from template. Not initializing protections."
	       fi
	    else
               warnmsg "Skipping Protections table initialization due to missing template: $ProtectsTemplate"
	    fi
	 else
            warnmsg "Skipping Protections table initialization because Protections table is already initialized."
	 fi
      else
         errmsg "Failed to create $P4USER user; tried to load this generated spec file:\\n$(cat "$SpecFile")"
      fi
      rm -f "$SpecFile"
   else
      errmsg "Failed to generate spec file for $P4USER user."
   fi
else
   warnmsg "Skipping $P4USER user creation; more than one user account exists."
fi

# Generate the Automation group with P4USER as member and owner.
if [[ "$(p4 group --exists -o Automation 2>&1)" =~ ^Group\ \' ]]; then
   SpecFile="$(mktemp)"
   if p4 --field Timeout=unlimited --field PasswordTimeout=unlimited --field Owners="$P4USER" --field Users="$P4USER" group -o Automation > "$SpecFile"; then
      msg "Creating group 'Automation'."
      p4 -s group -i < "$SpecFile" ||\
         errmsg "Failed to create Automation group; tried to load this generated spec file:\\n$(cat "$SpecFile")"
      rm -f "$SpecFile"
   else
      errmsg "Failed to generate spec file for Automation group."
   fi
else
   warnmsg "Skipping Automation group creation; group already exists."
fi

# The server.depot.root configurable was introduced in 2014.1.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2014.1" ]]; then
   p4 configure set server.depot.root="$DEPOTS" || ErrorCount+=1
fi

p4 configure set journalPrefix="$CHECKPOINTS/p4_${INSTANCE}" || ErrorCount+=1
p4 configure set dm.user.noautocreate=2 || ErrorCount+=1
p4 configure set dm.info.hide=1 || ErrorCount+=1

# For filesys.*.min configurables, use 5G defaults if we have 7G+ of
# space available, otherwise assume this is a demo-scale environment
# and use 20M
DiskSpaceAvail=$(df -k "$P4ROOT/" 2>/dev/null | grep / | awk '{print $4}')
if [[ "$DiskSpaceAvail" -ge "$MinKBFor5GLimits" ]]; then
   p4 configure set filesys.P4ROOT.min=5G || ErrorCount+=1
else
   p4 configure set filesys.P4ROOT.min=20M || ErrorCount+=1
fi

DiskSpaceAvail=$(df -k "$DEPOTS/" 2>/dev/null | grep / | awk '{print $4}')
if [[ "$DiskSpaceAvail" -ge "$MinKBFor5GLimits" ]]; then
   p4 configure set filesys.depot.min=5G || ErrorCount+=1
else
   p4 configure set filesys.depot.min=20M || ErrorCount+=1
fi

DiskSpaceAvail=$(df -k "$LOGS/" 2>/dev/null | grep / | awk '{print $4}')
if [[ "$DiskSpaceAvail" -ge "$MinKBFor5GLimits" ]]; then
   p4 configure set filesys.P4JOURNAL.min=5G || ErrorCount+=1
else
   p4 configure set filesys.P4JOURNAL.min=20M || ErrorCount+=1
fi

# Note: With the SDP structure, filesys.P4LOG.min=5G and
# filesys.TEMP.min=5G don't need to be set, as they are
# stored on the same volume as the journal and are thus
# accounted for with filesys.P4JOURNAL.min.

p4 configure set server=3 || ErrorCount+=1
p4 configure set monitor=2 || ErrorCount+=1

# For P4D 2013.2+, setting db.reorg.disable=1, which turns off
# dynamic database reorg, has been shown to significantly improve
# performance when Perforce databases (db.* files) are stored on
# some solid state storage devices, while not making a difference
# on others.
# shellcheck disable=SC2072
[[ "$P4D_VERSION" > "2013.1" ]] && p4 configure set db.reorg.disable=1 || ErrorCount+=1

# For P4D 2017.2.1594901 or greater, enable net.autotune.  For net.autotune
# to take effect, it must be enabled on both sides of a connection.  So, to
# get the full benefit, net.autotune must be enabled on all brokers, proxies,
# and clients.  See this KB article for details on fully enabling net.autotune:
# https://portal.perforce.com/s/article/15368
#
# For connections in which net.autotune is not enabled, the p4d default value
# of net.tcpsize takes effect.
#
# When P4D is older than 2014.2 but less than 2017.2.1594901, set net.tcpsize
# to 512k.  In 2014.2, the default value for net.tcpsize became 512k, a
# reasonable default, so it should not be set explicitly. Also, there are
# indications it can reduce performance if set when not needed.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" < "2014.2" ]]; then
   p4 configure set net.tcpsize=524288 || ErrorCount+=1
elif [[ "$P4D_VERSION" > "2017.2.1594900" ]]; then
   p4 configure set net.autotune=1 || ErrorCount+=1
   p4 configure unset net.tcpsize 2>/dev/null ||:
else
   p4 configure unset net.tcpsize 2>/dev/null ||:
fi

# For P4D 2016.2.1468155+, set db.monitor.shared = max value.
if [[ "$P4D_VERSION" > "2016.2.1468154" ]]; then
   # This is the number of 8k pages to set aside for monitoring,
   # which requires pre-allocation of sufficient RAM.  The default
   # is 256, or 2MB, enough for about 128 active/concurrent processes.
   # The max as of 2016.2 is 4096.  Setting db.monitor.shared=0
   # causes the db.monitor on disk to be used instead, which can
   # potentially be a bottleneck.
   p4 configure set db.monitor.shared=4096 || ErrorCount+=1
fi

p4 configure set net.backlog=2048 || ErrorCount+=1

p4 configure set lbr.autocompress=1 || ErrorCount+=1
p4 configure set lbr.bufsize=1M || ErrorCount+=1
p4 configure set filesys.bufsize=1M || ErrorCount+=1
p4 configure set serverlog.file.3="$LOGS/errors.csv" || ErrorCount+=1
p4 configure set serverlog.retain.3="$KEEPLOGS" || ErrorCount+=1

# The following are useful if using Interset Threat Detection with Perforce,
# or if P4AUDIT logs are otherwise desired.
# p4 configure set serverlog.file.4="$LOGS/audit.csv"
# p4 configure set serverlog.retain."4=$KEEPLOGS"

# Net Keepalives
p4 configure set net.keepalive.count=9
p4 configure set net.keepalive.disable=0
p4 configure set net.keepalive.idle=180
p4 configure set net.keepalive.interval=15

p4 configure set serverlog.file.7="$LOGS/events.csv" || ErrorCount+=1
p4 configure set serverlog.retain.7="$KEEPLOGS" || ErrorCount+=1
p4 configure set serverlog.file.8="$LOGS/integrity.csv" || ErrorCount+=1
p4 configure set serverlog.retain.8="$KEEPLOGS" || ErrorCount+=1

# Add a custom trigger for tracking trigger events:
p4 configure set serverlog.file.11="$LOGS/triggers.csv" || ErrorCount+=1
p4 configure set serverlog.retain.11="$KEEPLOGS" || ErrorCount+=1

SpecFile="${0%/*}/spec.depot.p4s"
if [[ -r "$SpecFile" ]]; then
   msg "Creating a depot named 'spec' of type 'spec'."
   p4 -s depot -i < "$SpecFile" ||\
      errmsg "Failed to create spec depot."
else
   warnmsg "Skipping spec depot creation due to missing depot spec file: $SpecFile"
fi

SpecFile="${0%/*}/unload.depot.p4s"
if [[ -r "$SpecFile" ]]; then
   msg "Creating a depot named 'unload' of unload 'unload'."
   p4 -s depot -i < "$SpecFile" ||\
      errmsg "Failed to create unload depot."
else
   warnmsg "Skipping unload depot creation due to missing depot spec file: $SpecFile"
fi

# Load shedding and other performance-preserving configurable.
# For p4d 2013.1+
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2013.1" ]]; then
   p4 configure set server.maxcommands=2500 || ErrorCount+=1
fi

# For p4d 2013.2+ -Turn off max* commandline overrides.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2013.2" ]]; then
   p4 configure set server.commandlimits=2 || ErrorCount+=1
fi

msg "See: https://portal.perforce.com/s/article/3867"
p4 configure set rpl.checksum.auto=1 || ErrorCount+=1
p4 configure set rpl.checksum.change=2 || ErrorCount+=1
p4 configure set rpl.checksum.table=1 || ErrorCount+=1

# Define number of login attempts before there is a delay, to thwart
# automated password crackers.  Default is 3; set to a higher value to
# be more friendly to humans without compromising the protection.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2013.1" ]]; then
   p4 configure set dm.user.loginattempts=7 || ErrorCount+=1
fi

# For p4d 2016.1 Patch 5+
# Enable a server with an expired temp license to start, albeit with limited
# functionality, so that license expiry doesn't make it impossible to perform
# license management via the front-door.  This configurable allows the server
# to be started regardless of a bad license, though users will still be blocked
# by license invalid messages.  Perpetual commercial licenses never expire;
# this configurable will not affect those.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2016.1.1408676" ]]; then
   p4 configure set server.start.unlicensed=1 || ErrorCount+=1
fi

# Starting with p4d 2015.1 Patch 5, disallow P4EXP v2014.2 (a client
# version known to misbehave) from connecting to the server.
# See:  http://portal.perforce.com/articles/KB/15014
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2015.1.1126924" ]]; then
   p4 configure set rejectList="P4EXP,version=2014.2" || ErrorCount+=1
fi

# For p4d 2011.1 thru 2015.1, set rpl.compress=3.  For p4d 2015.2+, set
# rpl.compress=4.  This setting compresses journal data only, which is
# almost always advantageous as it compresses well, while avoiding
# compression of archive data, which is a mixed bag in terms of performance
# benefits, and potentially a net negative.
# server.global.client.views - makes client views global in a commit/edge environment.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2015.2" ]]; then
   p4 configure set rpl.compress=4 || ErrorCount+=1
   p4 configure set server.global.client.views=1 || ErrorCount+=1
elif [[ "$P4D_VERSION" > "2011.1" ]]; then
   p4 configure set rpl.compress=3 || ErrorCount+=1
fi

# Starting with p4d 2016.2, enable these features.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2016.2" ]]; then
   p4 configure set server.locks.global=1 || ErrorCount+=1
   p4 configure set proxy.monitor.level=3 || ErrorCount+=1
fi

# Enable faster resubmit after failed submit.
p4 configure set submit.noretransfer=1 || ErrorCount+=1

# Recommended for Swarm
p4 configure set dm.shelve.promote=1 || ErrorCount+=1
p4 configure set dm.keys.hide=2 || ErrorCount+=1
p4 configure set filetype.bypasslock=1 || ErrorCount+=1

# Starting with p4d 2018.2 (as tech-preview, 2019.2 for GA), add best
# practices for Extensions.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2018.2" ]]; then
   p4 configure set server.extensions.dir="$LOGS"/p4-extensions || ErrorCount+=1
fi

# Set configurables to optimize for Helix Authentication Service (HAS)
# deployment. These will also affect behavior of older `auth-check-sso`
# triggers.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2018.2" ]]; then
   p4 configure set auth.sso.allow.passwd=1 || ErrorCount+=1
   p4 configure set auth.sso.nonldap=1 || ErrorCount+=1
fi

# Enable parallelization.
p4 configure set net.parallel.max=10 || ErrorCount+=1
p4 configure set net.parallel.threads=4 || ErrorCount+=1

# Limit max parallel syncs.
p4 configure set net.parallel.sync.svrthreads=150 || ErrorCount+=1

# Enable partitioned clients.
p4 configure set client.readonly.dir="$P4ROOT/client.readonly.dir"
p4 configure set client.sendq.dir="$P4ROOT/client.readonly.dir"

# Starting with p4d 2016.1, use auth.id to simplify ticket handling.
# After setting auth.id, login again.
# shellcheck disable=SC2072
if [[ "$P4D_VERSION" > "2016.1" ]]; then
   p4 configure set rpl.forward.login=1 || ErrorCount+=1
   p4 configure set auth.id="$P4SERVER" || ErrorCount+=1
   "$P4CBIN"/p4login
fi

# Set SDP version identifying info.
p4 counter SDP_DATE "$(date +'%Y-%m-%d')" || ErrorCount+=1
p4 counter SDP_VERSION "$SDP_VERSION" || ErrorCount+=1

msg "Restarting server to ensure all configurable changes take effect."

# Basic security features.
p4 configure set run.users.authorize=1 || ErrorCount+=1
p4 configure set security=4 || ErrorCount+=1

stop_p4d
start_p4d

msg "Logging in."
"$P4CBIN"/p4login

if [[ "$ErrorCount" -eq 0 && "$WarningCount" -eq 0 ]]; then
   msg "\\nAll processing completed successfully."
elif [[ "$ErrorCount" -eq 0 ]]; then
   warnmsg "Processing completed with no errors but $WarningCount warnings. Review the output carefully."
else
   errmsg "Processing completed, but with $ErrorCount errors and $WarningCount warnings. Review the output carefully."
fi

if [[ "$P4D_VERSION" > "2017.2.1594900" ]]; then
   msg "\\nThe net.autotune value has been set on the server.  To get the full benefit, it must also be\\nenabled on proxies, brokers, and clients as well."
fi

msg "\\nLog is: $LOGFILE"
