#!/bin/bash
# install_command-runner.sh
# ------------
# Installs "command-runner" from the already-unpacked files, sets up a systemd
# timer, adjusts SELinux (if present/enabled).
# It also checks the version of the new binary against any
# already installed command-runner.
#
# By default, runs in DRY-RUN mode (prints steps but does not apply changes).
# Use -y to perform a real install.
#
# Pre-requisite: Manually download and unpack the command-runner.tar.gz so that:
# - The new binary "command-runner-linux-<amd64|arm64>" is in the current directory.
# - "cmd_config.yaml" (if desired) is also in the current directory.
#
# Examples:
# sudo ./install_command-runner.sh -y
# sudo ./install_command-runner.sh -y -u myuser:mygroup -i prod
#
set -euo pipefail
################################################################################
# Ensure the script is run as root. If not, re-run it with sudo.
################################################################################
if [ "$EUID" -ne 0 ]; then
echo "This installer script must be run as root. Attempting to re-run with sudo..." >&2
exec sudo "$0" "$@"
fi
DEBUG_MODE=false
echo "Debug Mode = $DEBUG_MODE"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
x86_64|amd64)
BINARY_ARCH="amd64"
;;
aarch64|arm64)
BINARY_ARCH="arm64"
;;
*)
echo "Error: Unsupported architecture '$ARCH_RAW'. Supported: amd64, arm64" >&2
exit 1
;;
esac
BINARY_NAME="command-runner-linux-${BINARY_ARCH}"
PROD_REMOTE_BASE_URL="https://swarm.workshop.perforce.com"
REMOTE_INSTALL_FUNCTIONS_URL="${PROD_REMOTE_BASE_URL}/downloads/guest/perforce_software/command-runner/scripts/install_functions.sh"
REMOTE_BINARY_URL="${PROD_REMOTE_BASE_URL}/downloads/guest/perforce_software/command-runner/bin/${BINARY_NAME}"
REMOTE_CMD_CONFIG_URL="${PROD_REMOTE_BASE_URL}/downloads/guest/perforce_software/command-runner/configs/cmd_config.yaml"
DOWNLOADED_INSTALL_FUNCTIONS="false"
DOWNLOADED_BINARY_FALLBACK="false"
DOWNLOADED_BINARY_PATH=""
if [ ! -r "$SCRIPT_DIR/install_functions.sh" ]; then
echo "Warning: Unable to read $SCRIPT_DIR/install_functions.sh. Attempting download from production URL..." >&2
if ! command -v curl >/dev/null 2>&1; then
echo "Error: curl is required to download install_functions.sh" >&2
exit 1
fi
if ! curl -fSsL "$REMOTE_INSTALL_FUNCTIONS_URL" -o "$SCRIPT_DIR/install_functions.sh"; then
echo "Error: Failed to download install_functions.sh from $REMOTE_INSTALL_FUNCTIONS_URL" >&2
exit 1
fi
DOWNLOADED_INSTALL_FUNCTIONS="true"
NEW_BINARY_FALLBACK="$(pwd)/${BINARY_NAME}"
if ! curl -fSsL "$REMOTE_BINARY_URL" -o "$NEW_BINARY_FALLBACK"; then
echo "Error: Failed to download command-runner binary from $REMOTE_BINARY_URL" >&2
exit 1
fi
DOWNLOADED_BINARY_FALLBACK="true"
DOWNLOADED_BINARY_PATH="$NEW_BINARY_FALLBACK"
chmod +x "$NEW_BINARY_FALLBACK"
fi
source "$SCRIPT_DIR/install_functions.sh"
# Validate that required functions from install_functions.sh are available
for func in msg debug_msg bail run warnmsg print_args_box prompt_summary_and_confirm get_bin_version verify_instance detect_instance_from_service; do
if ! type -t "$func" >/dev/null 2>&1; then
echo "Error: Required function '$func' not found. install_functions.sh may not be loaded properly." >&2
exit 1
fi
done
print_args_box "$@"
################################################################################
# Default Settings
################################################################################
DRY_RUN="true" # Default to DRY-RUN unless overridden
TIMER_MODE="prod" # Default timer mode
P4_SDP_OWNER="perforce:perforce"
NONP4_SDP_OWNER="root:root" # Default for non-SDP installations
NEW_BINARY="$(pwd)/${BINARY_NAME}"
NEW_CONFIG="$(pwd)/cmd_config.yaml"
NEW_VERSION=""
OLD_SCRIPTNAME="report_instance_data.sh"
P4_BIN_DIR="/p4/common/site/bin"
NONP4_BIN_DIR="/usr/local/bin"
P4_BASE_DIR="/p4"
NONP4_BASE_DIR="/opt/perforce/command-runner"
DEFAULT_INSTANCE=""
SDP_MODE="true"
FORCE_INSTALL="false"
CURRENT_VERSION=""
SEND_USAGE="false"
VMAGENT_DIR="/var/vmagent"
################################################################################
# Usage / Help
################################################################################
usage() {
echo "Usage: $(basename "$0") [OPTIONS]"
echo ""
echo "Installs 'command-runner' from already-unpacked files (e.g., command-runner-linux-<amd64|arm64>"
echo "and cmd_config.yaml in the current directory), sets up a systemd timer, adjusts SELinux"
echo "(if present), and configures vmagent directory usage for metrics."
echo ""
echo "By default, this script runs in DRY-RUN mode (printing steps without applying changes)."
echo "Use the -y flag to actually apply changes."
echo ""
echo "Options:"
echo " -d PATH Destination path for the 'command-runner' binary"
echo " -c DIR Directory for cmd_config.yaml"
echo " -v DIR Directory for vmagent files (default: /var/vmagent)"
echo " -l FILE Path for command-runner.log"
echo " -u OWNER User:Group ownership"
echo " -i NAME Instance name (default: auto-detect)"
echo " -n Non-SDP mode (skip SDP-specific logic)"
echo " -y Actually apply changes (disable DRY-RUN)"
echo " -t Enable test mode (run every 5 minutes instead of daily)"
echo " -f Force install (non-interactive where applicable)"
echo " -h Show this help message"
echo ""
if [ "$SDP_MODE" = "true" ]; then
echo "Default SDP Mode Paths:"
echo " Owner/Group: $P4_SDP_OWNER"
echo " Binary Install Path: $P4_BIN_DIR/command-runner"
echo " Config Dir: $P4_BASE_DIR/common/config"
echo " Log File: $P4_BASE_DIR/_SDP_INSTANCE_NAME_/logs/command-runner__SDP_INSTANCE_NAME_.log"
echo " VMAgent Directory: /var/vmagent"
else
echo "Non-SDP Mode Paths:"
echo " Owner/Group: !! Please Define !!"
echo " Binary Install Path: $NONP4_BIN_DIR/command-runner"
echo " Config Dir: $NONP4_BASE_DIR"
echo " Log File: /var/log/command-runner.log"
echo " VMAgent Directory: /var/vmagent"
fi
echo ""
echo "Examples:"
echo " ./install_command-runner.sh # DRY-RUN by default"
echo " ./install_command-runner.sh -y # Real install"
echo " ./install_command-runner.sh -y -t -i 1 # Real install, test mode, instance '1'"
echo ""
exit 0
}
################################################################################
# Function to display the summary
################################################################################
get_bin_version
# If no arguments are provided, show help
if [ "$#" -eq 0 ]; then
usage
fi
################################################################################
# Parse Arguments
################################################################################
while getopts "nd:c:v:l:u:i:ytfh" opt; do
case $opt in
n) SDP_MODE="false" ;; # SDPP4 mode
d) INSTALL_PATH="$OPTARG" ;;
c) CONFIG_DIR="$OPTARG" ;;
v) VMAGENT_DIR="$OPTARG" ;;
l) LOG_FILE="$OPTARG" ;;
u) OWNER="$OPTARG" ;;
i) INSTANCE="$OPTARG" ;;
y) DRY_RUN="false" ;;
t) TIMER_MODE="test" ;;
f) FORCE_INSTALL="true" ;;
h) SEND_USAGE="true" ;; # Set SEND_USAGE to true when -h is provided
*) usage; exit 1 ;; # Handle invalid options
esac
done
# If SEND_USAGE is true, display the usage and exit
if [ "$SEND_USAGE" = "true" ]; then
usage
fi
# If force install is used, we ensure we are not in DRY-RUN.
if [ "$FORCE_INSTALL" = "true" ]; then
DRY_RUN="false"
fi
shift $((OPTIND -1))
##############################################################
# Set Defaults Based on SDP_MODE
##############################################################
if [ "$SDP_MODE" = "false" ]; then
msg "Using non-P4 defaults..."
DEFAULT_BIN_DIR="$NONP4_BIN_DIR"
DEFAULT_BASE_DIR="$NONP4_BASE_DIR"
DEFAULT_CONFIG_DIR="$NONP4_BASE_DIR"
DEFAULT_LOG_FILE="/var/log/command-runner.log"
DEFAULT_OWNER="$NONP4_SDP_OWNER"
else
msg "Using P4 defaults..."
DEFAULT_BIN_DIR="$P4_BIN_DIR"
DEFAULT_BASE_DIR="$P4_BASE_DIR"
DEFAULT_CONFIG_DIR="$P4_BASE_DIR/common/config"
DEFAULT_LOG_FILE="$DEFAULT_BASE_DIR/${DEFAULT_INSTANCE}/logs/command-runner_${DEFAULT_INSTANCE}.log"
DEFAULT_OWNER="$P4_SDP_OWNER"
fi
debug_msg "Defaults configured for $([ "$SDP_MODE" = "true" ] && echo "SDP" || echo "non-SDP") mode"
SERVICE_NAME="command-runner.service"
TIMER_NAME="command-runner.timer"
SERVICE_PATH="/etc/systemd/system/$SERVICE_NAME"
TIMER_PATH="/etc/systemd/system/$TIMER_NAME"
NEEDS_SERVICE_MIGRATION="false"
# Use custom arguments if provided; otherwise, apply defaults:
: "${INSTALL_PATH:="$DEFAULT_BIN_DIR"}"
#: "${INSTALL_PATH:="$DEFAULT_BIN_DIR/command-runner"}"
: "${CONFIG_DIR:="$DEFAULT_CONFIG_DIR"}"
: "${LOG_FILE:="$DEFAULT_LOG_FILE"}"
: "${INSTANCE:="$DEFAULT_INSTANCE"}"
: "${OWNER:="$DEFAULT_OWNER"}"
INSTALL_PATH="$INSTALL_PATH/command-runner"
REAL_BIN_DIR=$(dirname "$(readlink -f "$INSTALL_PATH" || echo "$INSTALL_PATH")")
# Ensure INSTANCE is set; if not, attempt to detect it from systemd or SDP structure
if [ "$SDP_MODE" = "true" ]; then
if [[ -z "$INSTANCE" ]]; then
msg "INSTANCE is not set. Attempting to parse from command-runner service..."
# Capture instance from service detection, but don't let failures overwrite INSTANCE
INSTANCE=$(detect_instance_from_service || true)
if [[ -z "$INSTANCE" ]]; then
debug_msg "No instance found in command-runner service. Attempting SDP structure detection..."
verify_instance # Ensure verify_instance sets INSTANCE correctly
else
msg "Using instance parsed from command-runner service: $INSTANCE"
fi
fi
# Check if INSTANCE is still empty, fail early
if [[ -z "$INSTANCE" ]]; then
msg "Error: No instance detected. Use -i <instance> to specify manually." >&2
exit 1
fi
fi
if [ "$SDP_MODE" = "true" ]; then
# Recalculate LOG_FILE with detected/specified instance
LOG_FILE="$DEFAULT_BASE_DIR/${INSTANCE}/logs/command-runner_${INSTANCE}.log"
else
debug_msg "SDP mode is $SDP_MODE no recalc on LOG_FILE"
fi
debug_msg "DEFAULT_INSTANCE: $DEFAULT_INSTANCE"
debug_msg "DEFAULT_LOG_FILE: $DEFAULT_LOG_FILE"
debug_msg "LOG_FILE: $LOG_FILE"
if [ -f "$SERVICE_PATH" ]; then
if grep -q -- "--metrics.config" "$SERVICE_PATH"; then
NEEDS_SERVICE_MIGRATION="true"
msg "Detected legacy metrics flag in existing service. Service will be updated to use --vmagent.dir."
fi
fi
if [ -z "$CURRENT_VERSION" ]; then
# Try to locate the command-runner binary using 'which'
INSTALL_PATH=$(which command-runner 2>/dev/null || echo "$INSTALL_PATH")
# If it's not found using 'which', fallback to the default INSTALL_PATH
if [ -x "$INSTALL_PATH" ]; then
current_raw="$("$INSTALL_PATH" --version 2>&1 | sed '/^[[:space:]]*$/d' | head -n1 || true)"
debug_msg "Raw version output from installed binary: $current_raw"
CURRENT_VERSION="$(echo "$current_raw" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')"
else
msg "Unable to find previous command-runner binary $INSTALL_PATH"
fi
fi
# If SEND_USAGE is true, display the usage
if [ "$SEND_USAGE" = "true" ]; then
usage
fi
################################################################################
# Step 1: Verify INSTANCE
################################################################################
verify_instance
if { [ "$DOWNLOADED_INSTALL_FUNCTIONS" = "true" ] || [ "$DOWNLOADED_BINARY_FALLBACK" = "true" ]; } && [ "$SDP_MODE" = "true" ]; then
P4_VARS_FILE="/p4/common/bin/p4_vars"
if [ -r "$P4_VARS_FILE" ] && [ -n "${INSTANCE:-}" ]; then
sdp_owner="$(bash -lc "source '$P4_VARS_FILE' '$INSTANCE' >/dev/null 2>&1 && printf '%s:%s' \"\${OSUSER:-}\" \"\${OSGROUP:-}\"")"
if [[ "$sdp_owner" != ":" && "$sdp_owner" == *:* ]]; then
if [ "$DOWNLOADED_INSTALL_FUNCTIONS" = "true" ]; then
run "chown '$sdp_owner' '$SCRIPT_DIR/install_functions.sh'" "Setting ownership on downloaded install_functions.sh"
fi
if [ "$DOWNLOADED_BINARY_FALLBACK" = "true" ] && [ -n "$DOWNLOADED_BINARY_PATH" ]; then
run "chown '$sdp_owner' '$DOWNLOADED_BINARY_PATH'" "Setting ownership on downloaded command-runner binary"
fi
else
warnmsg "Unable to resolve OSUSER/OSGROUP from $P4_VARS_FILE for instance '$INSTANCE'; leaving ownership unchanged on downloaded files"
fi
else
warnmsg "Cannot set ownership on downloaded files because $P4_VARS_FILE is unavailable or INSTANCE is unset"
fi
fi
debug_msg "Configuring service user and group"
# If OWNER is not set or is malformed, use 'perforce' as a fallback
if [[ "$OWNER" =~ ^([^:]+):([^:]+)$ ]]; then
SVC_USER="${BASH_REMATCH[1]}"
SVC_GROUP="${BASH_REMATCH[2]}"
debug_msg "Parsed OWNER='$OWNER' -> SVC_USER='$SVC_USER', SVC_GROUP='$SVC_GROUP'"
else
SVC_USER="$USER"
SVC_GROUP="$USER"
debug_msg "OWNER not set or invalid, defaulting to current user: SVC_USER='$SVC_USER', SVC_GROUP='$SVC_GROUP'"
fi
debug_msg "Ensuring the command is run with the correct user/group"
# Ensure the command is not running as root
if [[ "$SVC_USER" == "root" || "$SVC_GROUP" == "root" ]]; then
debug_msg "Detected root user in service definition, attempting to extract actual service user/group"
# Extract User and Group from the service file
if [[ -f "/etc/systemd/system/command-runner.service" ]]; then
debug_msg "Reading service file: /etc/systemd/system/command-runner.service"
SVC_USER=$(grep -E '^User=' "/etc/systemd/system/command-runner.service" | cut -d'=' -f2 | tr -d '[:space:]')
SVC_GROUP=$(grep -E '^Group=' "/etc/systemd/system/command-runner.service" | cut -d'=' -f2 | tr -d '[:space:]')
debug_msg "Extracted from service file: SVC_USER='$SVC_USER', SVC_GROUP='$SVC_GROUP'"
else
debug_msg "Service file not found, falling back to default user/group"
SVC_USER="perforce"
SVC_GROUP="perforce"
fi
# Default to "perforce" if extracted values are empty
SVC_USER=${SVC_USER:-perforce}
SVC_GROUP=${SVC_GROUP:-perforce}
debug_msg "Final user/group: SVC_USER='$SVC_USER', SVC_GROUP='$SVC_GROUP'"
# Ensure the service is not running as root
if [[ "$SVC_USER" == "root" || "$SVC_GROUP" == "root" ]]; then
msg "Error: command-runner cannot run as root. Please set a non-root user and group."
exit 1
fi
fi
msg "command-runner will run as: $SVC_USER:$SVC_GROUP"
debug_msg "Determining SELinux mode..."
# Determine SELinux mode (Ubuntu often won't have these commands)
if command -v getenforce &>/dev/null; then
SELinuxMode="$(getenforce 2>/dev/null || true)"
debug_msg "SELinux mode detected via getenforce: $SELinuxMode"
elif command -v sestatus &>/dev/null; then
SELinuxMode="$(sestatus 2>/dev/null | grep -i 'Current mode' | awk '{print $NF}')"
debug_msg "SELinux mode detected via sestatus: $SELinuxMode"
else
SELinuxMode="Not Installed"
debug_msg "SELinux tools not found. Assuming SELinux is not installed."
fi
################################################################################
# Step 2: Display Summary
################################################################################
prompt_summary_and_confirm
################################################################################
# Step 2a: Verify new binary
################################################################################
if [ ! -f "$NEW_BINARY" ]; then
bail "New binary '$NEW_BINARY' not found!"
fi
if [ ! -x "$NEW_BINARY" ]; then
msg "Making '$NEW_BINARY' executable."
chmod +x "$NEW_BINARY"
fi
if [ ! -f "$NEW_CONFIG" ]; then
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] '$NEW_CONFIG' not found. Would download from $REMOTE_CMD_CONFIG_URL and move to $CONFIG_DIR/cmd_config.yaml"
else
msg "'$NEW_CONFIG' not found. Attempting to download cmd_config.yaml from production URL..."
tmp_cfg="$(mktemp /tmp/cmd_config.yaml.XXXXXX)"
if curl -fSsL "$REMOTE_CMD_CONFIG_URL" -o "$tmp_cfg"; then
run "mkdir -p \"$CONFIG_DIR\"" "Ensuring config directory exists"
run "mv \"$tmp_cfg\" \"$CONFIG_DIR/cmd_config.yaml\"" "Moving downloaded cmd_config.yaml to $CONFIG_DIR"
NEW_CONFIG="$CONFIG_DIR/cmd_config.yaml"
msg "Downloaded and staged cmd_config.yaml at $NEW_CONFIG"
else
rm -f "$tmp_cfg"
msg "Warning: failed to download cmd_config.yaml from $REMOTE_CMD_CONFIG_URL. Skipping config file install."
fi
fi
fi
################################################################################
# Step 3: (Already in get_bin_version) – Parse new version
################################################################################
################################################################################
# Step 4: Check existing installed version
################################################################################
# Only compare if not test mode
if [ "$TIMER_MODE" != "test" ] && [ -n "$CURRENT_VERSION" ] && [ -n "$NEW_VERSION" ]; then
msg "Currently installed version: $CURRENT_VERSION"
msg "New version to install: $NEW_VERSION"
if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
if [ "$NEEDS_SERVICE_MIGRATION" = "true" ]; then
msg "Same binary version detected, but service migration is required. Continuing."
elif [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] Same version; skipping install."
exit 0
elif [ "$FORCE_INSTALL" = "true" ]; then
msg "Force install enabled; continuing despite matching versions."
else
read -rp "Reinstall anyway? [y/N]: " ans
if [[ ! "$ans" =~ ^[Yy]$ ]]; then
msg "Skipping."
exit 0
fi
fi
else
# version sort
if [[ "$(printf '%s\n' "$CURRENT_VERSION" "$NEW_VERSION" | sort -V | head -n1)" == "$CURRENT_VERSION" ]]; then
msg "Installed version ($CURRENT_VERSION) is older."
else
msg "Installed version ($CURRENT_VERSION) is newer."
read -rp "Downgrade anyway? [y/N]: " ans
if [[ ! "$ans" =~ ^[Yy]$ ]]; then
msg "Skipping."
exit 0
fi
fi
fi
fi
################################################################################
# Step 5: Backup and Remove Old Cron Entries for the Specified User
################################################################################
msg "Checking for old cron references to '$OLD_SCRIPTNAME' in crontab of '$SVC_USER'..."
CURRENT_CRON="$(sudo -u "$SVC_USER" crontab -l 2>/dev/null || :)"
OLD_LINES="$(echo "$CURRENT_CRON" | grep "$OLD_SCRIPTNAME" || :)"
if [ -n "$OLD_LINES" ]; then
msg "Found references in crontab for user '$SVC_USER':"
msg "$OLD_LINES"
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] Would create a backup and remove these lines from '$SVC_USER' crontab."
else
# Backup the current crontab
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
if [ "$SDP_MODE" = "true" ]; then
BACKUP_DIR="/p4/$INSTANCE/tmp"
else
BACKUP_DIR="/tmp"
fi
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
BACKUP_FILE="${BACKUP_DIR}/crontab_backup_${SVC_USER}_${TIMESTAMP}.bak"
msg "Backing up existing crontab to $BACKUP_FILE"
run "sudo -u \"$SVC_USER\" crontab -l > \"$BACKUP_FILE\"" "Saving crontab backup"
# Remove the lines containing the old script
NEW_CRON="$(echo "$CURRENT_CRON" | grep -v "$OLD_SCRIPTNAME")"
# Create a new temp file **owned by the correct user**
TMP_CRON=$(sudo -u "$SVC_USER" mktemp "/tmp/crontab_${SVC_USER}_XXXXXX")
# Ensure the perforce user has write access
echo "$NEW_CRON" | sudo -u "$SVC_USER" tee "$TMP_CRON" > /dev/null
run "sudo -u \"$SVC_USER\" crontab \"$TMP_CRON\"" "Updating crontab for '$SVC_USER' (old entries removed)"
rm -f "$TMP_CRON"
msg "Old cron jobs referencing '$OLD_SCRIPTNAME' have been removed. Backup saved at $BACKUP_FILE."
fi
else
msg "No cron references found for '$OLD_SCRIPTNAME' in '$SVC_USER' crontab."
fi
################################################################################
# Step 6: Prepare directories (Only for non-SDP mode)
################################################################################
if [ "$SDP_MODE" = "false" ]; then
# Create necessary directories in non-SDP mode (SDP assumes dirs already exist)
install_dir_parent="$(dirname "$INSTALL_PATH")"
log_dir="$(dirname "$LOG_FILE")"
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] Would mkdir -p $install_dir_parent $CONFIG_DIR $log_dir"
else
run "mkdir -p \"$install_dir_parent\" \"$CONFIG_DIR\" \"$log_dir\"" "Ensuring directories exist for non-SDP mode"
fi
else
msg "Skipping directory creation: Running in SDP mode (directories assumed to exist)."
fi
################################################################################
# Step 7: Install binary/config with backups
################################################################################
msg "Installing new binary to $INSTALL_PATH"
timestamp=$(date +'%Y%m%d-%H%M%S')
if [ -f "$INSTALL_PATH" ]; then
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY_RUN] Would backup existing $INSTALL_PATH -> $INSTALL_PATH.$timestamp"
msg "[DRY_RUN] Would stop services before replacing binary"
else
run "mv \"$INSTALL_PATH\" \"$INSTALL_PATH.$timestamp\"" "Backing up existing binary"
# Note: Service/timer will be reloaded after binary installation in Step 9
fi
fi
# Copy new binary
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] cp \"$NEW_BINARY\" \"$INSTALL_PATH\""
else
run "cp \"$NEW_BINARY\" \"$INSTALL_PATH\"" "Copying binary"
fi
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] chown \"$OWNER\" \"$INSTALL_PATH\""
msg "[DRY_RUN] chmod 755 \"$INSTALL_PATH\""
else
run "chown \"$OWNER\" \"$INSTALL_PATH\"" "Setting owner"
run "chmod 755 \"$INSTALL_PATH\"" "Setting permissions"
fi
if [ -f "$NEW_CONFIG" ]; then
msg "Installing cmd_config.yaml -> $CONFIG_DIR"
target_cfg="$CONFIG_DIR/cmd_config.yaml"
# Backup an existing one if present and source differs from destination
if [ -f "$target_cfg" ] && [ "$NEW_CONFIG" != "$target_cfg" ]; then
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] Backup $target_cfg -> $target_cfg.$timestamp"
else
run "cp \"$target_cfg\" \"$target_cfg.$timestamp\"" \
"Backing up existing cmd_config.yaml"
run "chown \"$OWNER\" \"$target_cfg.$timestamp\""
fi
fi
# Copy in the new config unless it has already been staged at destination
if [ "$NEW_CONFIG" = "$target_cfg" ]; then
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] cmd_config.yaml already staged at destination: $target_cfg"
msg "[DRY RUN] chown \"$OWNER\" \"$target_cfg\""
else
run "chown \"$OWNER\" \"$target_cfg\"" "Setting owner on staged cmd_config.yaml"
fi
else
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY_RUN] cp \"$NEW_CONFIG\" \"$target_cfg\""
msg "[DRY_RUN] chown \"$OWNER\" \"$target_cfg\""
else
run "cp \"$NEW_CONFIG\" \"$target_cfg\"" "Copying cmd_config.yaml"
run "chown \"$OWNER\" \"$target_cfg\"" "Setting owner"
fi
fi
else
msg "Skipping cmd_config.yaml install (file not found at $NEW_CONFIG)."
fi
################################################################################
# Step 8: SELinux (Ubuntu either won't have it, or it's disabled by default)
################################################################################
# Only run if it looks like SELinux is actually Enforcing/Permissive.
if [[ "$SELinuxMode" =~ ^(Enforcing|Permissive)$ ]]; then
msg "SELinux mode: $SELinuxMode"
if command -v semanage &>/dev/null; then
# Ensure the parent directories have the correct context
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] semanage fcontext -a -t var_t \"/p4/common(/.*)?\""
else
tmpfile=$(mktemp)
if ! semanage fcontext -a -t var_t "/p4/common(/.*)?" &>"$tmpfile"; then
if grep -q "already defined" "$tmpfile"; then
msg "SELinux: Context already defined for /p4/common."
else
cat "$tmpfile"
bail "Failed semanage fcontext for /p4/common"
fi
fi
rm -f "$tmpfile"
fi
# Ensure the binary has the correct context
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] semanage fcontext -a -t bin_t \"${REAL_BIN_DIR}/command-runner\""
else
tmpfile=$(mktemp)
if ! semanage fcontext -a -t bin_t "${REAL_BIN_DIR}/command-runner" &>"$tmpfile"; then
if grep -q "already defined" "$tmpfile"; then
msg "SELinux: Context already defined for ${REAL_BIN_DIR}/command-runner."
else
cat "$tmpfile"
bail "Failed semanage fcontext for ${REAL_BIN_DIR}/command-runner"
fi
fi
rm -f "$tmpfile"
fi
else
warnmsg "semanage not found. (SELinux context not updated.)"
fi
if command -v restorecon &>/dev/null; then
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] restorecon -vF \"${REAL_BIN_DIR}/command-runner\""
else
run "restorecon -vF \"${REAL_BIN_DIR}/command-runner\"" \
"Applying restorecon to the single binary"
fi
else
warnmsg "restorecon not found. (SELinux context not restored.)"
fi
else
msg "SELinux mode is '$SELinuxMode'. Skipping SELinux steps."
fi
################################################################################
# Step 9: systemd service/timer
################################################################################
RUNNER_CMD="$REAL_BIN_DIR/command-runner --instance=$INSTANCE \
--cmd.config=$CONFIG_DIR/cmd_config.yaml \
--vmagent.dir=$VMAGENT_DIR \
--log=$LOG_FILE"
msg "Creating systemd service: $SERVICE_PATH"
if [ "$DRY_RUN" = "true" ]; then
cat <<EOF
[DRY RUN] Service content:
[Unit]
Description=Command Runner TEST job
After=network.target
[Service]
Type=oneshot
ExecStart=$RUNNER_CMD --nodel --debug
User=$SVC_USER
Group=$SVC_GROUP
[Install]
WantedBy=multi-user.target
EOF
else
SERVICE_DESC="Command Runner ${TIMER_MODE^^} job"
run "cat <<EOF > \"$SERVICE_PATH\"
[Unit]
Description=$SERVICE_DESC
After=network.target
[Service]
Type=oneshot
ExecStart=$RUNNER_CMD
User=$SVC_USER
Group=$SVC_GROUP
[Install]
WantedBy=multi-user.target
EOF
" "Creating systemd service"
fi
msg "Creating systemd timer: $TIMER_PATH"
if [ "$DRY_RUN" = "true" ]; then
if [ "$TIMER_MODE" = "test" ]; then
cat <<EOF
[DRY RUN] Timer content:
[Unit]
Description=Run command-runner every 5 minutes for testing
[Timer]
OnBootSec=5m
OnUnitActiveSec=5m
Persistent=true
[Install]
WantedBy=timers.target
EOF
else
cat <<EOF
[DRY RUN] Timer content:
[Unit]
Description=Run command-runner daily at 23:00
[Timer]
OnCalendar=*-*-* 23:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
fi
else
if [ "$TIMER_MODE" = "test" ]; then
TIMER_DESC="Run command-runner every 5 minutes for testing"
TIMER_TRIGGER="OnBootSec=5m
OnUnitActiveSec=5m
Persistent=true"
else
TIMER_DESC="Run command-runner daily at 23:00"
TIMER_TRIGGER="OnCalendar=*-*-* 23:00:00
Persistent=true"
fi
run "cat <<EOF > \"$TIMER_PATH\"
[Unit]
Description=$TIMER_DESC
[Timer]
$TIMER_TRIGGER
[Install]
WantedBy=timers.target
EOF
" "Creating systemd timer"
fi
# Reload, enable, start timer
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] systemctl daemon-reload"
msg "[DRY RUN] systemctl enable $TIMER_NAME"
msg "[DRY RUN] systemctl start $TIMER_NAME"
else
run "systemctl daemon-reload" "Reloading systemd"
run "systemctl enable \"$TIMER_NAME\"" "Enabling timer"
run "systemctl start \"$TIMER_NAME\"" "Starting timer"
fi
################################################################################
# Step 11: Ownership for relevant files/dirs
################################################################################
msg "Ensuring correct ownership."
FILES_TO_CHOWN=(
"$CONFIG_DIR/cmd_config.yaml"
"$INSTALL_PATH"
"$LOG_FILE"
"$(dirname "$LOG_FILE")"
)
for file in "${FILES_TO_CHOWN[@]}"; do
if [ -e "$file" ]; then
run "chown $OWNER \"$file\"" "chown $file"
else
warnmsg "File or directory $file not found; skipping."
fi
done
################################################################################
# Step 12: Finished
################################################################################
installation_complete
if [ "$DRY_RUN" = "true" ]; then
msg "[DRY RUN] systemctl restart $TIMER_NAME &>/dev/null &"
msg "[DRY RUN] systemctl restart $SERVICE_NAME &>/dev/null &"
else
if [ ! -e /tmp/cmd-runner_installer.log ]; then
touch /tmp/cmd-runner_installer.log
chown "$SVC_USER:$SVC_GROUP" /tmp/cmd-runner_installer.log
chown "$SVC_USER:$SVC_GROUP" "$REAL_BIN_DIR/command-runner"
fi
run "mv /tmp/cmd-runner_installer.log /tmp/cmd-runner_installer.log.done"
update_sdp_instance_yaml
run "systemctl restart $TIMER_NAME &>/dev/null &" "Starting $TIMER_NAME in the background"
run "systemctl restart $SERVICE_NAME &>/dev/null &" "Starting $SERVICE_NAME in the background"
fi