#!/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
#------------------------------------------------------------------------------
#==============================================================================
# Declarations and Environment
set -u
declare ThisScript=${0##*/}
declare Version=1.5.1
declare ThisUser=
declare LiveSudoersFile=
declare TempSudoersFile=
declare BackupSudoersFile=
declare SDPInstance=
declare P4Service=
declare SystemCtlCmd=
declare SystemCtlPath=
declare LslocksPath=
declare LsofPath=
declare TmpFile=
declare -i LimitedSudoers=2
declare -a SystemCtlCmdList
declare -a P4InstanceList
declare -a SudoAliasList
declare Service=
declare TimerService=
declare -i ErrorCount=0
declare -i i=0
declare -i NoOp=1
declare -i Force=0
declare Log=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare RootHome=
#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
function errmsg () { msg "\nError: ${1:-Unknown Error}\n"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }
#------------------------------------------------------------------------------
# Function: emit_sudoers_cmd_alias
#
# Emit one sudoers Cmnd_Alias from the supplied command list. This intentionally
# keeps generated aliases smaller and logically grouped for compatibility with
# both classic sudo and sudo-rs.
function emit_sudoers_cmd_alias
{
local aliasName=${1:-}
shift || true
local -a cmdList=("$@")
local -i cmdCount=${#cmdList[@]}
local -i cmdIndex=0
local -i lastIndex=0
[[ -n "$aliasName" ]] || bail "emit_sudoers_cmd_alias called without an alias name."
[[ "$cmdCount" -gt 0 ]] || return 0
echo "Cmnd_Alias $aliasName = \\"
lastIndex=$((cmdCount - 1))
for cmdIndex in "${!cmdList[@]}"; do
if [[ "$cmdIndex" -lt "$lastIndex" ]]; then
echo " ${cmdList[$cmdIndex]}, \\"
else
echo -e " ${cmdList[$cmdIndex]}\n"
fi
done
SudoAliasList+=("$aliasName")
}
#------------------------------------------------------------------------------
# Function: emit_sudoers_user_spec
#
# Emit the final user specification that references the generated Cmnd_Alias
# definitions.
function emit_sudoers_user_spec
{
local osUser=${1:-}
local hostName=${2:-}
local -i aliasCount=${#SudoAliasList[@]}
local -i aliasIndex=0
local -i lastIndex=0
[[ -n "$osUser" ]] || bail "emit_sudoers_user_spec called without an OS user."
[[ -n "$hostName" ]] || bail "emit_sudoers_user_spec called without a host name."
[[ "$aliasCount" -gt 0 ]] || bail "No sudoers command aliases were generated."
echo "$osUser $hostName,localhost = (root) NOPASSWD: \\"
lastIndex=$((aliasCount - 1))
for aliasIndex in "${!SudoAliasList[@]}"; do
if [[ "$aliasIndex" -lt "$lastIndex" ]]; then
echo " ${SudoAliasList[$aliasIndex]}, \\"
else
echo " ${SudoAliasList[$aliasIndex]}"
fi
done
}
#------------------------------------------------------------------------------
# Function: validate_sudoers_file
#
# Validate a sudoers file with the active visudo implementation. On Ubuntu 26+
# this is commonly visudo-rs; on older platforms this is classic visudo.
function validate_sudoers_file
{
local sudoersFile=${1:-}
local validationOutput=
[[ -n "$sudoersFile" ]] || bail "validate_sudoers_file called without a file name."
[[ -r "$sudoersFile" ]] || bail "Cannot read sudoers file for validation: $sudoersFile"
validationOutput=$(mktemp)
if visudo -c -f "$sudoersFile" > "$validationOutput" 2>&1; then
rm -f "$validationOutput"
return 0
else
cat "$validationOutput"
rm -f "$validationOutput"
return 1
fi
}
#------------------------------------------------------------------------------
# Function: terminate
# shellcheck disable=SC2329
function terminate
{
# Disable signal trapping.
trap - EXIT SIGINT SIGTERM
[[ "$Log" != off ]] && \
msg "Log is: $Log\n${H1}\n"
# With the trap removed, exit.
exit "$ErrorCount"
}
#------------------------------------------------------------------------------
# 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 errorMessage=${2:-Unset}
if [[ "$errorMessage" != Unset ]]; then
msg "\n\nUsage Error:\n\n$errorMessage\n\n"
fi
msg "USAGE for $ThisScript v$Version:
$ThisScript {-full|-limited} [-y [-f]] [-L <log>] [-D]
or
$ThisScript [-h|-man]
"
if [[ $style == -man ]]; then
msg "
DESCRIPTION:
This script generates a sudoers file for the OS user that
owns /p4/common, which is expected to be the same user that the
Perforce Helix Core service runs as (typically 'perforce').
By default, the sudoers file is generated for review. If the '-y'
option is specified, the newly generated files is installed as
the live sudoers file by copying to /etc/sudoers.d/<OSUSER> and
adjusting permissions to 0400.
If '-full' (full sudo) is specified, a one-line sudoers file is
generated that looks something like this:
perforce ALL=(ALL) NOPASSWD: ALL
If '-limited' is specified, a limited sudoers file is generated
granting only necessary access to the perforce user.
If the sudoers file already exits, it will not be updated unless
'-f' (force) is provided.
The limited sudoers is recommended for production deployments.
OPTIONS:
-full
Specify '-full' to indicate that a sudoers file is to be generated
granting full root access to the server machine.
The '-full' or '-limited' option must be specified.
This option is discouraged as it is not as secure as the
'-limited' option.
-limited
Specify '-limited' to indicate that a sudoers file is to be
generated granting limited access to the server machine.
The '-full' or '-limited' option must be specified.
This option is recommended for optimal security.
-y This is confirmation to install the generated sudoers as the live
sudoers file.
-f Specify '-f' to overwrite an existing limited sudoers file,
/etc/sudoers.d/<OSUSER>
-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:
$RootHome/${ThisScript%.sh}.<Datestamp>.log
NOTE: This script is self-logging. That is, output displayed on the screen
is simultaneously captured in the log file.
-D Enable bash 'set -x' extreme debugging verbosity.
HELP OPTIONS:
-h Display short help message
-man Display man-style help message
EXAMPLES:
EXAMPLE 1: Generate a limited sudoers file for review.
cd /p4/sdp/Server/Unix/setup
./gen_sudoers.sh -limited
EXAMPLE 2: Generate a limited sudoers file and install it.
cd /p4/sdp/Server/Unix/setup
./gen_sudoers.sh -limited -y
EXAMPLE 3: Generate a limited sudoers file and install it, replacing an
existing one.
cd /p4/sdp/Server/Unix/setup
./gen_sudoers.sh -limited -f -y
EXAMPLE 4: Generate a full sudoers file and install it, replacing an
cd /p4/sdp/Server/Unix/setup
./gen_sudoers.sh -full -f -y
"
fi
exit 2
}
#==============================================================================
# Command Line Processing
#------------------------------------------------------------------------------
# Get the home for Root. This is reliably /root on Linux, but varies in UNIX
# distros. If /root does not exist, attempt detection with getent. As
# a fallback, use /tmp. This is only used for a location of the log file.
# It must be set before processing command line arguments because this value
# is used in the usage() function.
if [[ -d /root ]]; then
RootHome=/root
else
RootHome=$(getent passwd root | cut -d: -f6)
fi
RootHome=${RootHome:-/tmp}
#------------------------------------------------------------------------------
declare -i shiftArgs=0
set +u
while [[ $# -gt 0 ]]; do
case $1 in
(-h) RootHome=/root; usage -h;;
(-man) RootHome=/root; usage -man;;
(-L) Log="$2"; shiftArgs=1;;
(-f) Force=1;;
(-full) LimitedSudoers=0;;
(-limited) LimitedSudoers=1;;
(-y) NoOp=0;;
(-D) set -x;; # Debug; use 'set -x' mode.
(-*) usage -h "Unknown flag ($1).";;
(*) usage -h "Unknown parameter ($1).";;
esac
# Shift (modify $#) the appropriate number of times.
shift; while [[ $shiftArgs -gt 0 ]]; do
[[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
shiftArgs=$shiftArgs-1
shift
done
done
set -u
#==============================================================================
# Command Line Verification
[[ -n "${Log:-}" ]] || \
Log="$RootHome/${ThisScript%.sh}.$(date +'%Y%m%d-%H%M%S').log"
[[ "$LimitedSudoers" -eq 2 ]] &&
usage -h "Specify either '-limited' (limited sudoers) for '-full' (full sudoers)."
#==============================================================================
# Main Program
trap terminate EXIT SIGINT SIGTERM
if [[ "$Log" != off ]]; then
touch "$Log" || bail "Couldn't touch log file [$Log]."
# Redirect stdout and stderr to a log file.
exec > >(tee "$Log")
exec 2>&1
msg "${H1}\nLog is: $Log"
fi
ThisUser=$(id -n -u)
msg "Starting $ThisScript v$Version as $ThisUser@${HOSTNAME%%.*} on $(date)."
[[ "$ThisUser" == root ]] || bail "Run this as root, not $ThisUser."
# Determine list of SDP instances based in /p4/*/logs dirs.
i=0
# shellcheck disable=SC2012
for SDPInstance in $(ls -d /p4/*/logs/ 2>/dev/null|cut -d '/' -f 3); do
P4InstanceList[i]="$SDPInstance"
i+=1
done
SystemCtlCmdList[0]="start"
SystemCtlCmdList[1]="stop"
SystemCtlCmdList[2]="restart"
SystemCtlCmdList[3]="status"
SystemCtlCmdList[4]="cat"
SystemCtlCmdList[5]="enable"
SystemCtlCmdList[6]="disable"
SystemCtlCmdList[7]="is-enabled"
SystemCtlPath="$(command -v systemctl)"
LslocksPath="$(command -v lslocks)"
LsofPath="$(command -v lsof)"
OSUSER="$(stat -Lc %U /p4/common 2>/dev/null)"
[[ -n "$OSUSER" ]] || bail "Could not owner of /p4/common. Aborting."
[[ "$OSUSER" == root ]] && \
bail "Owner of /p4/common is unexpectedly root; it should be some other user e.g. 'perforce'. Aborting."
TempSudoersFile=$(mktemp)
LiveSudoersFile="/etc/sudoers.d/$OSUSER"
BackupSudoersFile="$RootHome/etc_sudoers.d_$OSUSER.bak.$(date +'%Y-%m-%d-%H%M%S')"
if [[ "$LimitedSudoers" -eq 1 ]]; then
{
declare -a ServiceCmdList
declare -a TimerCmdList
declare -a InstanceCmdList
declare -a AdminCmdList
declare -i InstanceAliasIndex=0
SudoAliasList=()
# General SDP-related systemd services.
ServiceCmdList=()
for Service in helix-auth monitor_locks node_exporter p4metrics p4prometheus vmagent; do
for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
ServiceCmdList+=("$SystemCtlPath $SystemCtlCmd $Service")
ServiceCmdList+=("$SystemCtlPath $SystemCtlCmd ${Service}.service")
done
done
emit_sudoers_cmd_alias P4_SVC_SYSTEMD_SERVICES "${ServiceCmdList[@]}"
# Timer-backed SDP services. Keep these separate from regular services
# because each timer-backed service has bare, .service, and .timer forms.
TimerCmdList=()
for TimerService in monitor_locks opt_perforce_sdp_backup; do
for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
TimerCmdList+=("$SystemCtlPath $SystemCtlCmd $TimerService")
TimerCmdList+=("$SystemCtlPath $SystemCtlCmd ${TimerService}.service")
TimerCmdList+=("$SystemCtlPath $SystemCtlCmd ${TimerService}.timer")
done
done
emit_sudoers_cmd_alias P4_SVC_SYSTEMD_TIMERS "${TimerCmdList[@]}"
# Instance-specific Helix Core services. Generate one alias per SDP
# instance so sites with multiple instances do not create an overly large
# comma-separated command list.
for SDPInstance in "${P4InstanceList[@]}"; do
InstanceAliasIndex+=1
InstanceCmdList=()
for P4Service in "p4d_${SDPInstance}" "p4broker_${SDPInstance}" "p4p_${SDPInstance}" "p4dtg_${SDPInstance}"; do
for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
InstanceCmdList+=("$SystemCtlPath $SystemCtlCmd $P4Service")
InstanceCmdList+=("$SystemCtlPath $SystemCtlCmd ${P4Service}.service")
done
done
emit_sudoers_cmd_alias "P4_SVC_INSTANCE_${InstanceAliasIndex}" "${InstanceCmdList[@]}"
done
# SDP administrative commands.
AdminCmdList=("/opt/perforce/helix-sdp/sdp/Server/Unix/p4/common/sdp_upgrade/sdp_upgrade.sh")
[[ -n "$LslocksPath" ]] && AdminCmdList+=("$LslocksPath")
[[ -n "$LsofPath" ]] && AdminCmdList+=("$LsofPath")
emit_sudoers_cmd_alias P4_SVC_ADMIN_TOOLS "${AdminCmdList[@]}"
emit_sudoers_user_spec "$OSUSER" "$HOSTNAME"
} > "$TempSudoersFile"
else
echo "$OSUSER ALL=(ALL) NOPASSWD: ALL" > "$TempSudoersFile"
fi
msg "Generated limited sudoers temp file:\n${H2}\n$(cat "$TempSudoersFile")\n${H2}\n"
if validate_sudoers_file "$TempSudoersFile"; then
msg "Generated sudoers temp file passed validation with active visudo."
else
bail "Generated sudoers temp file failed validation with active visudo."
fi
if [[ "$NoOp" -eq 0 ]]; then
if [[ -r "$LiveSudoersFile" ]]; then
msg "This file already exists: $LiveSudoersFile:\n${H2}\n$(cat "$LiveSudoersFile")\n${H2}\n"
if diff -q "$TempSudoersFile" "$LiveSudoersFile" > /dev/null 2>&1; then
if validate_sudoers_file "$LiveSudoersFile"; then
msg "\nVerified: The new generated sudoers file matches the currently installed one, and the installed file passes validation with active visudo.\nNo further processing required.\n"
exit 0
else
bail "Installed sudoers file matches the generated file but fails validation with active visudo: $LiveSudoersFile"
fi
fi
if [[ "$Force" -eq 1 ]]; then
msg "\nOverwriting existing live sudoers file due to -f."
msg "Creating backup file [$BackupSudoersFile]."
cp -p "$LiveSudoersFile" "$BackupSudoersFile" ||\
bail "Aborting: Failed to do: cp -p \"$LiveSudoersFile\" \"$BackupSudoersFile\""
else
bail "Aborting because sudoers file already exists (displayed above).\n\nUse '-f' to replace this file with the newly generated one."
fi
fi
msg "Installing $LiveSudoersFile"
mv -f "$TempSudoersFile" "$LiveSudoersFile" ||\
bail "Failed to move generated temp file in place as: $LiveSudoersFile"
chmod 0400 "$LiveSudoersFile" ||\
bail "Failed to do: chmod 0400 \"$LiveSudoersFile\""
if validate_sudoers_file "$LiveSudoersFile"; then
msg "Installed sudoers file passed validation with active visudo."
else
errmsg "Installed sudoers file failed validation with active visudo. Rolling back to prior version if available."
if [[ -r "$BackupSudoersFile" ]]; then
mv -f "$BackupSudoersFile" "$LiveSudoersFile" ||\
bail "Failed to do: mv -f \"$BackupSudoersFile\" \"$LiveSudoersFile\""
bail "Aborted after successful rollback to earlier version of [$LiveSudoersFile]."
else
rm -f "$LiveSudoersFile"
bail "Aborted after removing invalid newly generated sudoers file [$LiveSudoersFile]."
fi
fi
TmpFile=$(mktemp)
# shellcheck disable=SC2024
if sudo -l -U "$OSUSER" > "$TmpFile" 2>&1; then
if grep -q 'syntax error' "$TmpFile"; then
errmsg "The newly generated file failed a syntax check. Rolling back to prior version."
mv -f "$BackupSudoersFile" "$LiveSudoersFile" ||\
bail "Failed to do: mv -f \"$BackupSudoersFile\" \"$LiveSudoersFile\""
bail "Aborted after successful rollback to earlier version of [$LiveSudoersFile]."
else
msg "Newly generated sudoers file passed a syntax check."
fi
else
bail "Could not test generated sudoers file. If needed, the backup file is: $BackupSudoersFile"
fi
msg "\nNew sudoers file successfully installed.\n"
else
msg "\nBecause -y was not specified, not installing generated file as: $LiveSudoersFile"
fi
rm -f "$TempSudoersFile"
exit "$ErrorCount"
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #16 | 33022 | C. Thomas Tyler |
Simplify usage of 'force_start' option with systemd. This entailed enhancing docs for gen_sudoers.sh (no functional change), as well as adding documentation about force starts, p4d_init.log, and documentation of the journal corruption detection handling. Minor style consistency changes were made in p4broker_base and p4p_base. Fixes SDP-837. #review-33023 @jclucas @mark_zinthefer @robert_cowham @tom_tyler |
||
| #15 | 32942 | C. Thomas Tyler |
gen_sudoers.sh v1.5.1 * Changed OSUSER detection from: stat -c %U /p4/common to: stat -Lc %U /p4/common so ownership is determined from the SDP common directory target rather than from the /p4/common symlink itself. * Improves compatibility on platforms where symbolic-link ownership differs from target-directory ownership. |
||
| #14 | 32941 | C. Thomas Tyler |
Fixed an edge case where a broken SDP install with root-owned /p4/common would cause gen_sudoers.sh to generate a sudoers file for root rather than the appropriate OS user. Added ShellCheck silender for terminate() function (called by trap). Cosmetic code fix for newlines (introduced due to ShellCheck bug). |
||
| #13 | 32898 | C. Thomas Tyler |
Updated gen_sudoers.sh to add monitor_locks.{service,timer}. #review-32899 @robert_cowham @tom_tyler |
||
| #12 | 32440 | C. Thomas Tyler |
Appended ",localhost" to sudoers entry to make it more robust. Fixes SDP-1342. |
||
| #11 | 32376 | C. Thomas Tyler | Added 'vmagent' service, part of P4Prometheus (for P4RA customers). | ||
| #10 | 32345 | C. Thomas Tyler |
Added sudo entry for p4metrics in gen_sudoers.sh. #review @robert_cowham |
||
| #9 | 31716 | C. Thomas Tyler |
Added support for 'p4 monitor -L'. Changes: * Added 'sudo /usr/bin/lsof' to sudoers commands. * Added 'monitor.lsof=sudo /usr/bin/lsof -F pln' as configurable in configurables.cfg, so that 'ccheck.sh' (the best practice configurables check) will search for it. * Updated configure_new_server.sh to set this value. * Added 'lsof' so list of SDP standard pacakges in install_sdp.sh. Note that 'sudo' is reliably found in /usr/bin/lsof across Linux distros, so this was deemed safe to hard-code. Fixes: SDP-1265 (Feature): Support 'p4 monitor -L' by default. |
||
| #8 | 31445 | C. Thomas Tyler |
Removed setcap handling code from upgrade.sh now that we can rely on AmbientCapabilities in the systemd unit file to enable the OOM Killer defense feature in p4d. Thus, the limited sudoers no longer requires setcap/getcap commands. Fixed various typos with aspell. #review-31446 |
||
| #7 | 31313 | C. Thomas Tyler |
Added '.service' alternatives for all systemctl services, because command completion on some platforms appends the '.service' suffix, and the sudo entries will not work unless '.service' is listed. So for example, now both of these will work: $ sudo systemctl start p4d_1 $ sudo systemctl start p4d_1.service Added support for the opt_perforce_sdp_backup service and timer, including adding explicit support for enabling and disabling the timer. |
||
| #6 | 31303 | C. Thomas Tyler |
Fixed issue where invalid sudoers file could be generated if setcap and getcap are not available, e.g. on SuSE 15 systems. Added helix-auth to standard list of managed services. |
||
| #5 | 31071 | C. Thomas Tyler | Refined man page output. | ||
| #4 | 31068 | C. Thomas Tyler |
Adjusted to avoid using HOME for root; using /root as a fixed value. |
||
| #3 | 30944 | ftp |
Added full path to sdp_upgrade.sh in immutable area to list of scripts that can be called with limited sudo. |
||
| #2 | 30782 | C. Thomas Tyler |
Added new install_sdp.sh script and supporting documentation. The new install_sdp.sh makes SDP independent of the separate Helix Installer software (the reset_sdp.sh script). The new script greatly improves the installation experience for new server machines. It is ground up rewrite of the reset_sdp.sh script. The new script preserves the desired behaviors of the original Helix Installer script, but is focused on the use case of a fresh install on a new server machine. With this focus, the scripts does not have any "reset" logic, making it completely safe. Added various files and functionalityfrom Helix Installer into SDP. * Added firewalld templates to SDP, and added ufw support. * Improved sudoers generation. * Added bash shell templates. This script also installs in the coming SDP Package structure. New installs use a modified SDP structure that makes it so the /p4/sdp and /p4/common now point to folders on the local OS volume rather than the /hxepots volume. The /hxdepots volume, which is often NFS mounted, is still used for depots and checkpoints, and for backups. The new structure uses a new /opt/perforce/helix-sdp structure under which /p4/sdp and /p4/common point. This structure also contains the expaneded SDP tarball, downloads, helix_binaries, etc. This change represents the first of 3-phase rollout of the new package structure. In this first phase, the "silent beta" phase, the new structure is used for new installations only. This phase requires no changes to released SDP scripts except for mkdirs.sh, and even that script remains backward-compatible with the old structure if used independently of install_sdp.sh. If used with install_sdp.sh, the new structure is used. In the second phase (targeted for SPD 2024.2 release), the sdp_upgrade.sh script will convert existing installations to the new structure. In the third phase (targeted for SDP 2025.x), this script will be incorporated into OS pacakge installations for the helix-sdp package. Perforce internal wikis have more detail on this change. #review-30783 |
||
| #1 | 30681 | C. Thomas Tyler |
Added gen_sudoers.sh script to generate a sudoers file for perforce OSUSER. This generates a more secure limited sudoers file. Previously, adding a sudoers entry for the OSUSER (usually 'perforce') was done only by the Helix Installer. In the Helix Installer variant, a single "one-size-filts-all" sudoers file was used, with the following characteristics: * The instances for Helix Core services were referenced with a '*' wildcard to match all SDP instances, which has since been determined to introduce a vulnerability. In this new script, the wildcard is replaced with separate entries for each SDP instance. * There were entries for all known paths of utilities like lslocks, setcap, and getcap. This new script generates the correct path valid for the current machine. With this change, the functionality will be available in the SDP directly. This new gen_sudoers.sh script can be called by mkdirs.sh directly to update the sudoers file each time a new SDP instance is added, if the new '-fs' (full sudo) or '-ls' (limited sudoers) entries are used. There is no change to the default behavior of mkdirs.sh; only a change if new options are utilized. This script comes with docs and examples for the new script as well as doc changes for mkdirs.sh. (Also added missing documentation for the '-no_enable' option). Further changes needed: * Add doc reference in SDP_Guide.Unix.adoc |