install_command-runner.sh #4

  • //
  • guest/
  • perforce_software/
  • command-runner/
  • scripts/
  • install_command-runner.sh
  • View
  • Commits
  • Open Download .zip Download (29 KB)
#!/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
# Change User Description Committed
#4 33016 Robert Cowham Try get_volume_info.sh
#3 32930 Robert Cowham Set ownership properly
#2 32926 Robert Cowham Update version
#1 32922 Robert Cowham New version of command-runner