#!/bin/bash +x
# shellcheck disable=SC2155
#==============================================================================
# Copyright @ 2016 Puppet, Inc.
# Redistribution prohibited.
# Address: 308 SW 2nd Ave., 5th Floor Portland, OR 97204
# Phone: (877) 575-9775
# Email: info@puppet.com
#===============================================================================

#===[ Summary ]=================================================================

# This program installs Puppet Enterprise. Run this file to start the
# installation or run with a "-h" option to display help.

#===[ Conventions ]=============================================================

# See style guide

#===============================================================================

#===[ Global Varables ]=========================================================
# Directory paths
readonly INSTALLER_DIR="$(readlink -f "$(dirname "${0}")")"
readonly LOGFILE_DIR='/var/log/puppetlabs/installer'
readonly LOGFILE_NAME="$(date +'%Y-%m-%dT%H.%M.%S%z').install.log"
readonly LOGFILE_PATH="${LOGFILE_DIR}/${LOGFILE_NAME}"
readonly ENVIRONMENTPATH='/opt/puppetlabs/server/data/environments'
readonly ENVIRONMENT='enterprise'
readonly PACKAGE_DIR='/opt/puppetlabs/server/data/packages/public'
readonly PUPPET_DIR='/opt/puppetlabs/puppet'
readonly PUPPET_BIN_DIR='/opt/puppetlabs/puppet/bin'
readonly SERVER_DIR="/opt/puppetlabs/server"
readonly OLD_OPT_DIR="/opt/puppet"
readonly INSTALLER_BIN_DIR='/opt/puppetlabs/installer/bin'
readonly ENTERPRISE_CONF_DIR="/etc/puppetlabs/enterprise/conf.d"
readonly ENTERPRISE_CONF_PATH="${ENTERPRISE_CONF_DIR}/pe.conf"
readonly DEFAULT_CONF_FILE_PATH="${INSTALLER_DIR?}/conf.d/pe.conf"
readonly RESET_URL_FILE="/opt/puppetlabs/server/share/installer/reset-url"
if [ -d "$LOGFILE_DIR" ]; then
  export LOGGING_INITIALIZED=true
else
  export LOGGING_INITIALIZED=false
fi

# Docs links variables
# These variables should all start with LINK_
# shellcheck disable=SC1090,SC1091
source "${INSTALLER_DIR?}/links"

# gettext i18n variables
export TEXTDOMAINDIR
readonly TEXTDOMAINDIR="${INSTALLER_DIR}/locales"
export TEXTDOMAIN="puppet-enterprise"

# Common variables
# NOTE: Upgrading to 2019.0 and later requires MCO to be disabled which was
#       not implemented until 2018.1.1.
readonly MINIMUM_SUPPORTED_VERSION="2019.8.0"
# Format: '%Y.%m.%d'
readonly EFFECTIVE_DATE='2022.10.20'
# At the moment we can only attempt to rollback if the version we were
# upgrading from has a pe-modules package.
readonly SCRIPT_NAME="$(basename "${0}")"
readonly PE_REPO_NAME='puppet_enterprise'

readonly APT_GET_ARGS="-o Apt::Get::Purge=false \
-o Dpkg::Options::='--force-confold' \
-o Dpkg::Options::='--force-confdef' \
-o Dpkg::Options::='--force-confmiss' \
--no-install-recommends"

# Source bootstrap-metadata. This file is generated at compose time
# and contains the following variables:
# PE_BUILD_VERSION, AIO_AGENT_VERSION and PLATFORM_TAG
readonly BOOTSTRAP_METADATA="${INSTALLER_DIR?}/packages/bootstrap-metadata"
if [[ -f $BOOTSTRAP_METADATA ]]; then
  # the below shellcheck comment is needed to make shellcheck happy,
  # as it attempts to actually source the file, which is dynamically
  # generated at compose time.
  #
  # shellcheck source=/dev/null
  source "${BOOTSTRAP_METADATA}"
else
  err "packages/bootstrap-metadata file missing"
  exit 1
fi

readonly BUILD_DATE_FILE="${SERVER_DIR}/effective_date"
if [[ -f $BUILD_DATE_FILE ]]; then
    readonly INSTALLED_EFFECTIVE_DATE=$(cat "${BUILD_DATE_FILE}")
else
    # We are setting this to 0 because lexicographically,
    # 0 is less than our version strings and will allow us
    # to upgrade if the existing version does not have a
    # build date file.
    readonly INSTALLED_EFFECTIVE_DATE="0"
fi

# The following globals are set by option flags and will be set to
# readonly after parsing.
CONF_FILE_PATH=""
IS_DEBUG='false'
IS_SUPPRESS_OUTPUT='false'
IS_INTERACTIVE_MODE='true'
PE_MODULES_NAME='pe-modules'
IS_PREP_ONLY='false'
IS_SKIPPING_DB_CHECK='false'
#===============================================================================

#===[ Gettext and Logging Functions ]===========================================
# Initialize gettext for i18n
#
# Falls back to a fake gettext() if gettext.sh can't be found so that the
# user can still see output if they don't have gettext.
if type gettext.sh >/dev/null 2>&1; then
  # shellcheck disable=SC1091
  . gettext.sh
else
  echo "[WARNING] Unable to load gettext.sh."
  echo "[WARNING] Please install the gettext package if you would like translated instructions."

  # Fake gettext function
  gettext() {
    printf "%s" "${1}"
  }

  # Fake eval_gettext function
  eval_gettext() {
    # eval_gettext() as defined in gettext.sh uses envsubst, so we will reproduce
    # that usage here.
    # shellcheck disable=SC2006,SC2046,SC2086
    expanded="$(eval echo \"$1\")"
    gettext "${expanded}"
  }
fi

#=== FUNCTION ================================================================
#        NAME:  display
# DESCRIPTION:  Echo and log
#   ARGUMENTS:
#               1. Message to be echoed and logged
#=============================================================================
display() {
  local message="${1?}"
  if $LOGGING_INITIALIZED; then
    echo "${message}" 2>&1 | tee -a "${LOGFILE_PATH?}"
  else
    echo "${message}" 2>&1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  gettext_display
# DESCRIPTION:  Translate, echo and log. Prefer this function over
#               display if the text should be translated.
#   ARGUMENTS:
#               1. Message to be translated, echoed and logged
#=============================================================================
gettext_display() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  display "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_display
# DESCRIPTION:  Translate, echo and log. Prefer this function over
#               display if the text should be translated and the text
#               contains parameters that need to be interpreted.
#   ARGUMENTS:
#               1. Message to be translated, echoed and logged
#=============================================================================
eval_gettext_display() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  display "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  display_announcement
# DESCRIPTION:  Announce something to the user
#   ARGUMENTS:
#               1. Message to be announced
#=============================================================================
display_announcement() {
  local message="${1?}"
  display_new_line
  display "## ${message}"
  display_new_line
}

#=== FUNCTION ================================================================
#        NAME:  gettext_display_announcement
# DESCRIPTION:  Announce something to the user. Prefer this function over
#               display_announcement if the text should be translated.
#   ARGUMENTS:
#               1. Message to be translated and announced
#=============================================================================
gettext_display_announcement() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  display_announcement "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_display_announcement
# DESCRIPTION:  Announce something to the user. Prefer this function over
#               display_announcement if the text should be translated.
#   ARGUMENTS:
#               1. Message to be translated and announced
#=============================================================================
eval_gettext_display_announcement() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  display_announcement "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  display_indented
# DESCRIPTION:  Display indented text to the user.
#   ARGUMENTS:
#               1. Message to be displayed
#               2. Number of 2-space-wide indents to use
#=============================================================================
display_indented() {
  levels="${2}"
  if [ -z "${levels}" ]; then
    levels="2"
  fi
  for ((i=0; i<levels; i++)); do echo -n '  '; done
  display "${1}"
}

#=== FUNCTION ================================================================
#        NAME:  gettext_display_indented
# DESCRIPTION:  Translate and display indented text to the user. Prefer this
#               function over display_indented if the text should be translated.
#   ARGUMENTS:
#               1. Message to be translated and displayed
#               2. Number of 2-space-wide indents to use
#=============================================================================
gettext_display_indented() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  display_indented "${translation}" "${2}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_display_indented
# DESCRIPTION:  Translate and display indented text with parameters to substitute
#               to the user. Prefer this function over display_indented if the
#               text should be translated.
#   ARGUMENTS:
#               1. Message to be translated and displayed
#               2. Number of 2-space-wide indents to use
#=============================================================================
eval_gettext_display_indented() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  display_indented "${translation}" "${2}"
}

#=== FUNCTION ================================================================
#        NAME:  display_line_break
# DESCRIPTION:  Display a line break
#=============================================================================
display_line_break() {
  display "============================================================="
}

#=== FUNCTION ================================================================
#        NAME:  display_new_line
# DESCRIPTION:  Display an 80 character line break
#=============================================================================
display_new_line() {
  display ""
}

#=== FUNCTION ================================================================
#        NAME:  err
# DESCRIPTION:  A function so as to provide a shorthand error command
#=============================================================================
err() {
  if $LOGGING_INITIALIZED; then
    printf "%s [ERROR]: %s\n" "$(get_timestamp)" "$@" | tee -a "${LOGFILE_PATH?}" >&2
  else
    printf "%s [ERROR]: %s\n" "$(get_timestamp)" "$@" >&2
  fi
}

#=== FUNCTION ================================================================
#        NAME:  gettext_err
# DESCRIPTION:  A function to log translated error followed by the error as
#               written in script. Prefer this function if the text should
#               be translated.
#=============================================================================
gettext_err() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  err "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_err
# DESCRIPTION:  A function to log translated error followed by the error as
#               written in script. Prefer this function if the text should
#               be translated and the text contains parameters to be interpreted..
#=============================================================================
eval_gettext_err() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  err "${translation}"
}

#=== FUNCTION ================================================================
#        NAME:  gettext_wrapped_err
# DESCRIPTION:  A function to log an error wrapped in an arbitrary string
#      PARAMS:
#               1. Error to be translated and logged
#               2. String to pre and postfix to error
#=============================================================================
gettext_wrapped_err() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  err "${2} ${translation} ${2}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_wrapped_err
# DESCRIPTION:  A function to log translated error followed by the error as
#               written in script. Prefer this function if the text should
#               be translated and the text contains parameters to be interpreted..
#=============================================================================
eval_gettext_wrapped_err() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  err "${2} ${translation} ${2}"
}

#=== FUNCTION ================================================================
#        NAME:  get_timestamp
# DESCRIPTION:  Provides a way of getting a RFC 3339 timestamp
#=============================================================================
get_timestamp() {
  local timestamp
  timestamp=$(date +'%Y-%m-%dT%H:%M:%S.%3N%:z')
  echo "${timestamp:0:29}"
}

#=== FUNCTION ================================================================
#        NAME:  get_unix_timestamp
# DESCRIPTION:  Provides a way of getting a unix style timestamp
#=============================================================================
get_unix_timestamp() {
  local timestamp
  timestamp=$(date +'%s')
  echo "${timestamp}"
}

#=== FUNCTION ================================================================
#        NAME: prepare_logging
# DESCRIPTION: Prepares logging
#=============================================================================
prepare_logging() {
  mkdir -p "${LOGFILE_DIR?}"
  touch "${LOGFILE_PATH?}"
  chmod 600 "${LOGFILE_PATH?}"
  export LOGGING_INITIALIZED=true
}
#===============================================================================

#===[ User Input Functions ]====================================================
#=== FUNCTION ================================================================
#        NAME: user_confirmation $TEXT
# DESCRIPTION: Confirm with user that they actually want to proceed with
#              $TEXT. Assumes yes by default, unless a second argument is
#              passed in, then default no.
#=============================================================================
user_confirmation() {
  if [[ -z "${1}" ]]; then
    gettext_err "FATAL: user_confirmation() called without an argument"
    exit 1
  fi

  local default="[Yn]"
  if [[ -n "${2}" ]]; then
    default="[yN]"
  fi

  local text="${1}"
  echo -en "\n ${text} ${default}"
  fail_if_quiet
  read -r interview_answer
  if [[ -z "${interview_answer}" && -z "${2}" ]] || [[ "${interview_answer}" =~ ^[Yy][Ee]?[Ss]? ]]; then
    return 0
  else
    return 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  gettext_user_confirmation
# DESCRIPTION:  Translate and Confirm with user that they actually want to proceed with
#               $TEXT. Assumes yes by default.
#   ARGUMENTS:
#               1. Message to be translated and displayed
#               2. (Optional) When given, default answer is No instead of Yes.
#      RETURN:
#               0 if no answer or yes
#               1 otherwise
#=============================================================================
gettext_user_confirmation() {
  # shellcheck disable=SC2006
  translation="`gettext \"${1}\"`"
  user_confirmation "${translation}" "${2}"
}

#=== FUNCTION ================================================================
#        NAME:  eval_gettext_user_confirmation
# DESCRIPTION:  Translate and Confirm with user that they actually want to proceed with
#               $TEXT. Assumes yes by default.
#   ARGUMENTS:
#               1. Message to be translated and displayed
#               2. (Optional) When given, default answer is No instead of Yes
#      RETURN:
#               0 if no answer or yes
#               1 otherwise
#=============================================================================
eval_gettext_user_confirmation() {
  # shellcheck disable=SC2006
  translation="`eval_gettext \"${1}\"`"
  user_confirmation "${translation}" "${2}"
}

#=== FUNCTION ================================================================
#        NAME: confirm_proceed_with_install_or_upgrade
# DESCRIPTION: Confirm with user that they actually want to proceed with
#              the upgrade and exits if not. Assumes yes by default.
#=============================================================================
confirm_proceed_with_install_or_upgrade() {
  local install_method=$1
  local verb="installation of"
  if [[ -n "${CURRENT_PE_VERSION}" ]]; then
    verb="repair of"
    if upgrading ; then
      # For some reason, shellcheck thinks this isn't being used
      # shellcheck disable=SC2034
      verb="upgrade to"
    fi
  fi
  local pe_conf_text=''
  if [[ "${install_method}" != "0" ]]; then
    # shellcheck disable=SC2034
    pe_conf_text=" using the pe.conf at ${CONF_FILE_PATH}"
  fi
  eval_gettext_user_confirmation "Proceed with the \${verb} \${NEW_PE_VERSION}\${pe_conf_text}?"
  return $?
}
#===============================================================================

#===[ Print Message Functions ]=================================================
#=== FUNCTION ================================================================
#        NAME:  ca_dir_migration
# DESCRIPTION:  Checks if a user needs to migrate the CA dir, and if so,
#               direct them on how to do it. This should be run post-upgrade.
#=============================================================================
ca_dir_migration() {
  if [[ ! -d /etc/puppetlabs/puppetserver/ca ]]; then
    gettext_display "* Migrate your CA directory to the new location."
    gettext_display "  The CA directory residing in /etc/puppetlabs/puppet/ssl/ca is now deprecated and needs to be migrated to the new location at /etc/puppetlabs/puppetserver/ca."
    gettext_display "  To do this, run the following commands:"
    gettext_display "    /opt/puppetlabs/bin/puppet resource service pe-puppetserver ensure=stopped"
    gettext_display "    /opt/puppetlabs/bin/puppetserver ca migrate"
    gettext_display "    /opt/puppetlabs/bin/puppet resource service pe-puppetserver ensure=running"
    gettext_display "    /opt/puppetlabs/bin/puppet agent -t"
  fi
}

#=== FUNCTION ================================================================
#        NAME:  display_pe_header
# DESCRIPTION:  Displays the PE header
#=============================================================================
display_pe_header() {
  display_line_break
  gettext_display_indented "Puppet Enterprise Installer" '2'
  display_line_break
}

#=== FUNCTION ================================================================
#        NAME:  display_analytics_warning
# DESCRIPTION:  Displays the analytics warning if DISABLE_ANALYTICS is not
#               defined.
#=============================================================================
display_analytics_warning() {
  if [[ -z "${DISABLE_ANALYTICS}" ]]; then
    gettext_display_announcement "Installer analytics are enabled by default."
    gettext_display "To disable, set the DISABLE_ANALYTICS environment variable and rerun this script. For example, \"sudo DISABLE_ANALYTICS=1 ./puppet-enterprise-installer\"."
    gettext_display "If puppet_enterprise::send_analytics_data is set to false in pe.conf, this is not necessary and analytics will be disabled."
    display_new_line
  fi
}

#=== FUNCTION ==================================================================
#        NAME:  print_usage
# DESCRIPTION:  Print usage instructions.
#===============================================================================
print_usage() {
  flag_indentation="2"
  flag_desc_indentation="4"
  display "USAGE: ${SCRIPT_NAME?} [-c CONF_FILE] [-D] [-h] [-q] [-V] [-y] [-p] [-s]"
  display_new_line
  gettext_display "OPTIONS:"
  display_new_line
  display_indented "-c <PATH_TO_FILE>" "${flag_indentation}"
  gettext_display_indented "Use pe.conf at <PATH_TO_FILE>." "${flag_desc_indentation}"
  display_new_line
  gettext_display_indented "If you have a pre-existing pe.conf, the installer will overwrite it if you use this flag." "${flag_desc_indentation}"
  gettext_display_indented "Note that installer will create a backup of the pre-existing pe.conf before overwriting it." "${flag_desc_indentation}"
  display_indented "-D" "${flag_indentation}"
  gettext_display_indented "Display debugging information." "${flag_desc_indentation}"
  display_indented "-h" "${flag_indentation}"
  gettext_display_indented "Display this help." "${flag_desc_indentation}"
  display_indented "-q" "${flag_indentation}"
  gettext_display_indented "Run in quiet mode; the installation process is not displayed." "${flag_desc_indentation}"
  display_indented "-V" "${flag_indentation}"
  gettext_display_indented "Display very verbose debugging information." "${flag_desc_indentation}"
  display_indented "-y" "${flag_indentation}"
  eval_gettext_display_indented "Assume yes/default and bypass any prompts for user input." "${flag_desc_indentation}"
  gettext_display_indented "Ensure that your pe.conf file is valid before using this flag." "${flag_desc_indentation}"
  display_indented "-p" "${flag_indentation}"
  gettext_display_indented "Prepare the system for future install." "${flag_desc_indentation}"
  gettext_display_indented "Will install packages and modules, but not run the final configure command." "${flag_desc_indentation}"
  display_indented "-s" "${flag_indentation}"
  gettext_display_indented "Skip any PostgreSQL checks, not recommended." "${flag_desc_indentation}"
}

#=== FUNCTION ================================================================
#        NAME: show_welcome_message
# DESCRIPTION: Display an introductory message informing the user of express
#              install or repair/upgrade, and to cancel and rerun with options
#              if this install is not desired.
#=============================================================================
show_welcome_message() {
  display_pe_header
  display_new_line
  gettext_display "Welcome to the Puppet Enterprise installer!"
  display_new_line
  gettext_display "Unless Puppet has otherwise agreed in writing, all software is subject to the terms and conditions of Puppet's Software"
  gettext_display "License Agreement located at https://puppet.com/legal."
  display_new_line
  # Display warning if CentOS 8 was detected in detect_platform()
  if [[ -n "${CENTOS_8}" ]]; then
    eval_gettext_display "*** WARNING: You are installing Puppet Enterprise on CentOS 8. This OS has reached end of life and is no longer supported. You can continue to use it at your own risk."
    display_new_line
  fi
  if [[ -n "${CURRENT_PE_VERSION}" ]]; then
    eval_gettext_display "We've detected an existing Puppet Enterprise \${CURRENT_PE_VERSION} install."
  else
    gettext_display "This will install a standard configuration for Puppet Enterprise."
    gettext_display "Make sure to use 'puppet infrastructure console_password' at the end of install to set the console admin password."
    display_new_line
    gettext_display "Advanced option: If you would like to use a customized pe.conf file, exit the installer and rerun with the '-c' flag."
    gettext_display "The pe.conf file is a HOCON formatted file that declares parameters and values needed to install and configure PE."
    gettext_display "Please see the installation docs for more information on installing with custom configurations."
    gettext_display "${LINK_INSTALL_PE}"
  fi
}
#===============================================================================

#===[ Utility Functions ]=======================================================
#=== FUNCTION ================================================================
#        NAME:  cmd
# DESCRIPTION:  A portable 'which' command
#   ARGUMENTS:
#               1. Name of the program to see if it is installed
#=============================================================================
cmd() {
  local program_name="${1?}"
  hash "${program_name?}" >&/dev/null;
}

#=== FUNCTION ================================================================
#        NAME: is_version_less_than
# DESCRIPTION: Compares two version strings lexicographically to determine
#              if the new one is an upgrade
#          $1: Version string for the currently installed product
#          $2: Version string of the product we want to install
#          $3: Padding to use for version string. (Optional)
#=============================================================================
is_version_less_than() {
    local current=$1
    local new=$2
    local padding="${3:-9999}"
    local current_version=""
    local new_version=""

    current_version=$(padded_version_string "${current}" "${padding}")
    new_version=$(padded_version_string "${new}" "${padding}")

    if [[ "${current_version}" < "${new_version}" ]] || [[ "${current_version}" == "${new_version}" ]]; then
        return 0
    else
        return 1
    fi
}

#=== FUNCTION ================================================================
#        NAME: simplify_agent_version
# DESCRIPTION: Takes a puppet-agent version string and returns only the first
#              three X Y Z digits. Strips off any fourth digit and git sha
#          $1: The agent version we want to simplify
#=============================================================================
simplify_agent_version() {
  pattern='[0-9]+\.[0-9]+\.[0-9]+'
  [[ $1 =~ $pattern ]] && echo "${BASH_REMATCH[0]}"
}

#=== FUNCTION ================================================================
#        NAME: padded_version_string
# DESCRIPTION: Takes a version string and pads it to make it easy for
#              comparisons. Only pads to two places.
#          $1: Version string
#          $2: Padding for version string. (Optional)
#=============================================================================
padded_version_string() {
    local version=$1
    local padding="${2:-9999}"
    local number_version=""

    number_version="${version/rc/}"
    # Replacing '-' with '.' and splitting version string on "."
    # to get major, minor, patch parts.
    IFS='.' read -r -a PARTS <<< "${number_version//-/.}"

    # Removing git sha, which is always in the last part
    if [[ "${version}" =~ [.-]g ]]; then
        unset "PARTS[${#PARTS[@]}-1]"
    fi

    # Adding additional padding for PE-22545
    # Some customers may be running an RC version and upgrading to
    # a release version will fail. Padding all the way to RC's, but
    # only for release versions (3 element versions like 2017.3.0).
    #
    # Skipping empty strings so a fresh install (empty string) will still
    # compare properly.
    if [ ${#PARTS[@]} == 3 ]; then
        for ((i=${#PARTS[@]}; i<5; i++)); do
            PARTS+=("${padding}")
        done
    fi
    printf "%.4d" "${PARTS[@]#0}"
}

#=== FUNCTION ================================================================
#        NAME:  run
# DESCRIPTION:  A function so as to provide a shorthand run and log command
#   ARGUMENTS:
#               1. Command to run
#=============================================================================
run() {
  local run_command="${1?}"
  # shellcheck disable=SC2006
  run_command_translation="`gettext 'Running command'`"
  printf "%s ${run_command_translation}: %s\n" "$(get_timestamp)" "${run_command}" 2>&1 | tee -a "${LOGFILE_PATH?}"
  ( eval "${run_command?}" ) 2>&1 | tee -a "${LOGFILE_PATH?}"
  # Return the status of the command, not tee
  return "${PIPESTATUS[0]}"
}

#=== FUNCTION ================================================================
#        NAME:  run_with_output
# DESCRIPTION:  A function so as to provide a shorthand run and log command,
#               and also return output from execution.
#   ARGUMENTS:
#               1. Command to run
#=============================================================================
run_with_output() {
  local run_command="${1?}"
  # shellcheck disable=SC2006
  run_command_translation="`gettext 'Running command'`"
  # shellcheck disable=SC2069
  printf "%s ${run_command_translation}: %s\n" "$(get_timestamp)" "${run_command}" 2>&1 >> "${LOGFILE_PATH?}"
  local result=
  result=$( eval "${run_command?}" 2>&1)
  local exitcode=$?
  echo "${result}" | tee -a "${LOGFILE_PATH?}"
  return "${exitcode}"
}
#===============================================================================

#===[ Workflow Support Functions ]=======================================================
#=== FUNCTION ================================================================
#        NAME: backup_previous_build_info
# DESCRIPTION: Copy current pe_build and puppet/VERSION (if they exist)
#              so we can rollback system configuration after failure.
#=============================================================================
backup_previous_build_info() {
  local pe_build_file="${SERVER_DIR?}/pe_build"
  if [ -f "${pe_build_file}" ]; then
    run "cp ${pe_build_file} ${SERVER_DIR?}/pe_build.bak"
    run "cp ${PUPPET_DIR?}/VERSION ${SERVER_DIR?}/puppet-agent-version.bak"
  fi
}

#=== FUNCTION ================================================================
#        NAME:  clean_reset_url_file
# DESCRIPTION:  Removes reset_url_file
#=============================================================================
clean_reset_url_file() {
  if [[ -f "${RESET_URL_FILE}" ]]; then
    rm -f ${RESET_URL_FILE}
  fi
}

#=== FUNCTION ================================================================
#        NAME: current_pe_agent_version
# DESCRIPTION: Determine the agent version already installed on the system
#              (if any.) Otherwise, return an empty string.
#=============================================================================
current_pe_agent_version() {
    if [[ -f "${PUPPET_DIR?}/VERSION" ]]; then
        build_version=$(cat "${PUPPET_DIR?}/VERSION")
        echo "${build_version}"
    else
        echo ""
    fi
}

#=== FUNCTION ================================================================
#        NAME: current_pe_build
# DESCRIPTION: Determine the build version of PE already installed on the
#              system (if any.) Otherwise, return an empty string.
#=============================================================================
current_pe_build() {
  if [[ -f "${SERVER_DIR}/pe_build" ]]; then
    build_version=$(cat "${SERVER_DIR}/pe_build")
    echo "${build_version}"
  else
    echo ""
  fi
}

#=== FUNCTION ================================================================
#        NAME: current_pe_version
# DESCRIPTION: Determine the version of PE already installed on the system
#              (if any.) Otherwise, return an empty string.
#=============================================================================
current_pe_version() {
  if [[ -f "${SERVER_DIR}/pe_version" ]]; then
    installed_version=$(cat "${SERVER_DIR}/pe_version")
    echo "${installed_version}"
  else
    echo ""
  fi
}

#=== FUNCTION ================================================================
#        NAME: new_pe_version
# DESCRIPTION: Get the version of PE from the tarball
#=============================================================================
new_pe_version() {
  cat "${INSTALLER_DIR?}/VERSION" 2> /dev/null
}

#=== FUNCTION ================================================================
#        NAME: new_pe_build
# DESCRIPTION: Get the build of PE from bootstrap metadata in tarball
#=============================================================================
new_pe_build() {
  echo "${PE_BUILD_VERSION?}"
}

#=== FUNCTION ================================================================
#        NAME: detect_platform
# DESCRIPTION: Parses /etc/os-release when it exists and falls back to other
#              methods if it doesn't. Sets the PLATFORM_PACKAGING and
#              LOCAL_PLATFORM_TAG variables, and exits 1 if it can't determine
#              the relevant bits or detects an unsupported platform.
#=============================================================================
detect_platform() {
  if [ -f /etc/os-release ]; then
    # These files may have unquoted spaces in the "pretty" fields even though
    # the spec says otherwise
    # shellcheck source=/dev/null
    source <(sed 's/ /_/g' /etc/os-release)
    # Note that we previously had a "READABLE_PLATFORM_NAME" variable for use when
    # notifying a user about a platform soon to be deprecated. If we need this in
    # the future, use PRETTY_NAME from this file instead.
    case "${ID?}" in
      rhel|centos|ol|scientific|almalinux|rocky)
        PLATFORM_NAME='el'
        PLATFORM_PACKAGING='rpm'
        IS_FIPS="$(cat /proc/sys/crypto/fips_enabled)"
        if [ "${IS_FIPS?}" == "1" ]; then
          PLATFORM_NAME='redhatfips'
        fi
        PLATFORM_RELEASE="${VERSION_ID%%.*}"
        # Warning for Centos 8
        if [ "${ID}" == "centos" ] && [ "${PLATFORM_RELEASE}" == "8" ] ; then
          CENTOS_8=1
        fi
        ;;
      amzn)
        PLATFORM_NAME='el'
        PLATFORM_PACKAGING='rpm'
        case "${VERSION_ID?}" in
          2)
            PLATFORM_RELEASE=7
            ;;
          *)
            gettext_err "Amazon Linux ${VERSION_ID} is not currently supported. Visit ${LINK_SUPPORTED_OS} for a list of supported platforms."
            exit 1
            ;;
        esac
        ;;
      sles)
        PLATFORM_NAME='sles'
        PLATFORM_PACKAGING='zypper'
        PLATFORM_RELEASE="${VERSION_ID%%.*}"
        ;;
      ubuntu)
        PLATFORM_NAME='ubuntu'
        PLATFORM_PACKAGING='apt'
        PLATFORM_RELEASE="${VERSION_ID}"
        ;;
      *)
        gettext_err "Unknown platform ${ID}. Visit ${LINK_SUPPORTED_OS} for a list of supported platforms."
        exit 1
        ;;
    esac
  elif [ -f /etc/redhat-release ]; then
    PLATFORM_PACKAGING='rpm'
    PLATFORM_NAME='el'
    IS_FIPS="$(cat /proc/sys/crypto/fips_enabled)"
    if [ "${IS_FIPS?}" == "1" ]; then
      PLATFORM_NAME='redhatfips'
    else
      PLATFORM_NAME='el'
    fi
    # Release - take first digits after ' release ' only.
    PLATFORM_RELEASE="$(sed 's/.*\ release\ \([[:digit:]]\+\).*/\1/g;q' /etc/redhat-release)"
    # Check for CentOS 8 and throw a warning in show_welcome_message() after printing license info
    if [ "${PLATFORM_NAME}"  == "el" ] && [ "${PLATFORM_RELEASE}" == "8" ] ; then
      if grep -qi 'centos' /etc/redhat-release; then
        CENTOS_8='1'
      fi
    fi
  elif [ -f /etc/system-release ] && grep -q Amazon /etc/system-release; then
    PLATFORM_PACKAGING='rpm'
    PLATFORM_NAME='el'
    t_image_name=$(grep image_name /etc/image-id | cut -d\" -f2 | cut -d- -f1)
    if [ -z "$t_image_name" ]; then
      gettext_err "Unable to parse Amazon Linux version info from /etc/image-id"
      exit 1
    else
        if [ "$t_image_name" == "amzn2" ]; then
            PLATFORM_RELEASE=7
        else
            PLATFORM_RELEASE=6
        fi
    fi
  elif [ -f /etc/SuSE-release ]; then
    PLATFORM_PACKAGING='zypper'
    PLATFORM_NAME='sles'
    PLATFORM_RELEASE="$(grep VERSION /etc/SuSE-release | sed 's/^VERSION = \(\d*\)/\1/')"
  elif cmd "lsb_release"; then
    # Older Ubuntu doesn't have a *-release file,
    # and lsb_release isn't installed by default on other platforms
    # so fall back to this after looking for the others.
    local lsb_release_id
    lsb_release_id=$(lsb_release -is)
    if [ "${lsb_release_id}" = "Ubuntu" ]; then
      PLATFORM_PACKAGING='apt'
      PLATFORM_NAME='ubuntu'
      PLATFORM_RELEASE="$(lsb_release -rs)"
    fi
  fi

  PLATFORM_ARCHITECTURE="$(uname -m)"
  case "${PLATFORM_ARCHITECTURE?}" in
    x86_64)
        case "${PLATFORM_NAME?}" in
            ubuntu | debian )
                PLATFORM_ARCHITECTURE=amd64
                ;;
        esac
        ;;
  esac

  if [ -z "${LOCAL_PLATFORM_TAG:-""}" ]; then
    LOCAL_PLATFORM_TAG="${PLATFORM_NAME?}-${PLATFORM_RELEASE?}-${PLATFORM_ARCHITECTURE?}"
  fi

  if [ -z "${PLATFORM_PACKAGING:-""}" ]; then
    gettext_err "Unknown Platform. Visit ${LINK_SUPPORTED_OS}"
    exit 1
  else
    readonly PLATFORM_PACKAGING
    readonly LOCAL_PLATFORM_TAG
  fi
}

#=== FUNCTION ================================================================
#        NAME:  disable_puppet_agent
# DESCRIPTION:  Sets the disable lockfile preventing puppet agent runs.
#
#               This routine also traps INT TERM EXIT and QUIT signals,
#               registering enable_puppet_agent so that we are
#               reasonably sure to remove the disable lockfile before
#               exiting.  Kill cannot be trapped.
#=============================================================================
disable_puppet_agent() {
  if [ -e "${PUPPET_BIN_DIR?}/puppet" ]; then
    trap enable_puppet_agent EXIT
    trap "enable_puppet_agent; exit 1" INT TERM QUIT
    run "${PUPPET_BIN_DIR?}/puppet agent \
      --disable='puppet-enterprise-installer preparing to configure node'"
    if ! wait_for_agent_lock; then
      gettext_err "Puppet lockfile still present."
      gettext_err "Aborting install."
      exit 1
    fi
  fi
}

#=== FUNCTION ================================================================
#        NAME:  enable_puppet_agent
# DESCRIPTION:  Clears the disable lockfile, allowing puppet agent runs.
#=============================================================================
enable_puppet_agent() {
  [ -e "${PUPPET_BIN_DIR?}/puppet" ] && run "${PUPPET_BIN_DIR?}/puppet agent --enable"
}

#=== FUNCTION ================================================================
#        NAME:  fail_if_quiet
# DESCRIPTION:  Checks for the use of the quiet flag, and exits 1
#               if it is set.
#=============================================================================
fail_if_quiet(){
  if is_quiet_mode; then
    rollback_upgrade
    gettext_display "You are running in quiet mode but your configuration requires interaction(s). Exiting."
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  feature_flag
# DESCRIPTION:  Returns true if the feature flag parameter is true in pe.conf
#=============================================================================
feature_flag() {
  local flag="${1?}"
  grep "${flag?}" "${CONF_FILE_PATH?}" | grep -q 'true'
}

#=== FUNCTION ================================================================
#        NAME:  generate_versioned_package_list
# DESCRIPTION:  Converte a generic package=version list to a platform
#               specific one suitable for rpm, apt or zypper
#          $@:  package list
#=============================================================================
generate_versioned_package_list() {
  declare -a packages
  for package in "$@"; do
    local name=${package%=*}
    local version
    if [ "${name}" != "${package}" ]; then
      version=${package#*=}
    else
      version=''
    fi
    local parsed_package

    if [ -z "${version}" ]; then
      parsed_package="$package"
    else
      case "${PLATFORM_PACKAGING?}" in
        rpm) parsed_package="${name}-${version}" ;;
        apt) parsed_package="${name}=${version}*" ;;
        *)   parsed_package="$package" ;;
      esac
    fi
    # shellcheck disable=SC2206
    packages=(${packages[*]} $parsed_package)
  done
  echo "${packages[*]}"
}

#=== FUNCTION ================================================================
#        NAME:  ensure_agent_upgraded
# DESCRIPTION:  Compares aio with current agent version.
#               If not the same, then the agent didn't upgrade and need to
#               roll back the upgrade.
#=============================================================================
ensure_agent_upgraded() {
  expected_agent_version=$(simplify_agent_version "$AIO_AGENT_VERSION")
  actual_agent_version=$(simplify_agent_version "$(current_pe_agent_version)")

  if [ "$expected_agent_version" != "$actual_agent_version" ] ; then
    rollback_upgrade
    eval_gettext_err " The puppet-agent failed to upgrade, current puppet-agent version is ${actual_agent_version}"
    eval_gettext_err " expected the puppet-agent version to be ${expected_agent_version}"
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  install_error
# DESCRIPTION:  Notifies user install failed and exits with given code
#=============================================================================
install_error() {
  gettext_wrapped_err "There were problems during the application of the installation catalog." "!!"
  eval_gettext_wrapped_err "Review the logs at \${LOGFILE_PATH} and resolve any issues you can find." "!!"
  gettext_wrapped_err "After fixing any errors, re-run the installer to complete the installation or upgrade." "!!"
  exit "$1"
}

#=== FUNCTION ================================================================
#        NAME:  install_packages
# DESCRIPTION:  Invoke platform specific package manager to install a
#               list of packages.
#          $@:  package list
#=============================================================================
install_packages() {
  declare -a packages
  IFS=" " read -r -a packages <<< "$(generate_versioned_package_list "$@")"

  case "${PLATFORM_PACKAGING?}" in
    rpm)
      if upgrading ; then
        run "yum upgrade -y ${packages[*]}"
      else
        run "yum install -y ${packages[*]}"
      fi
      ;;
    zypper)
      run "zypper --non-interactive install --repo ${PE_REPO_NAME?} ${packages[*]}"
      ;;
    apt)
      run "DEBIAN_FRONTEND=noninteractive apt-get install -y ${APT_GET_ARGS?} ${packages[*]}"
      ;;
  esac
  local result=$?
  if [ "${result?}" != 0 ]; then
    eval_gettext_err "Unable to install packages: \${packages}"
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  install_puppet_agent
# DESCRIPTION:  Installs puppet agent using preconfigured local package
#               repository. See setup_installer_runtime
#=============================================================================
install_puppet_agent() {
  gettext_display_announcement "We're installing the Puppet Agent..."
  install_packages "puppet-agent=${AIO_INSTALL_VERSION:?}"
}


#=== FUNCTION ================================================================
#        NAME: is_external_postgres
# DESCRIPTION: Return 0 if this install contains an external postgres node,
#              111 otherwise
#=============================================================================
is_external_postgres() {
  if [ "${IS_SKIPPING_DB_CHECK?}" = "true" ] ; then
    return 111
  fi
  run "${INSTALLER_BIN_DIR}/is_external_postgres.rb"
  return $?
}

#=== FUNCTION ================================================================
#        NAME: is_db_node
# DESCRIPTION: Return 0 if this postgres is on this node, 111 otherwise
#=============================================================================
is_db_node() {
  if [ "${IS_SKIPPING_DB_CHECK?}" = "true" ] ; then
    return 111
  fi
  run "${INSTALLER_BIN_DIR}/running_on_dbnode.rb"
  return $?
}

#=== FUNCTION ================================================================
#        NAME: is_interactive_mode
# DESCRIPTION: Return true if the interactive flag has been set
#=============================================================================
is_interactive_mode() {
  if [[ "${IS_INTERACTIVE_MODE}" == 'true' ]]; then
    fail_if_quiet
    return 0
  else
    return 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: is_quiet_mode
# DESCRIPTION: Return true if the quiet flag has been set
#=============================================================================
is_quiet_mode() {
  if [[ "${IS_SUPPRESS_OUTPUT}" == 'true' ]]; then
    return 0
  else
    return 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  move_win64_msi
# DESCRIPTION:  Moves the 64 bit windows and windowsfips msi package
#               into place so pe_repo will work seamlessly.
#=============================================================================
move_win64_msi() {
  for platform in windows windowsfips; do
    local base_pkg_dir="${PACKAGE_DIR?}/${PE_BUILD_VERSION?}"
    local win64_installer_pkg_path="${INSTALLER_DIR?}/packages/${platform}-x86_64"
    local win64_pkg_dir="${base_pkg_dir?}/${platform}-x86_64-${AIO_AGENT_VERSION?}"

    run "mkdir -p ${win64_pkg_dir?}"
    run "cp ${win64_installer_pkg_path?}/puppet-agent-${AIO_INSTALL_VERSION?}-x64.msi ${win64_pkg_dir?}/puppet-agent-x64.msi"
  done
}

#=== FUNCTION ================================================================
#        NAME:  psql_data_error
# DESCRIPTION:  Prints an error message, rolls back the upgrade, and exits the
#               installer process.
#=============================================================================
psql_data_error() {
    gettext_err "Something went wrong obtaining psql data. Rolling back the upgrade"
    rollback_upgrade
    exit 1
}

#=== FUNCTION ================================================================
#        NAME:  remove_packages
# DESCRIPTION:  Invoke platform specific package manager to remove a
#               list of packages.
#          $@:  package list
#=============================================================================
remove_packages() {
  declare -a packages
  # shellcheck disable=SC2206
  packages=($@)

  case "${PLATFORM_PACKAGING?}" in
    rpm)
      run "yum remove -y ${packages[*]}"
      ;;
    zypper)
      run "zypper --non-interactive remove ${packages[*]}"
      ;;
    apt)
      run "apt-get remove -y ${packages[*]}"
      ;;
  esac
}

#=== FUNCTION ================================================================
#        NAME:  rollback_upgrade
# DESCRIPTION:  Resets repository configuration, downgrades packages,
#               resets pe_build file; but only if we were installing
#               over a previous installation.
#
#               Note: this can only be used before PE services have been
#               upgraded and restarted. At that point, the services will
#               have begun migrating their database schemas and cannot
#               be rolled back.
#
#               Currently we are only using this function to reset the
#               puppet-agent, pe-modules and pe-installer package after
#               puppet-infra configure validation has failed and before
#               a catalog has been applied to begin the rest of the
#               upgrade.
#=============================================================================
rollback_upgrade() {
  local previous_pe_build_file="${SERVER_DIR?}/pe_build.bak"
  if [ -f "${previous_pe_build_file}" ]; then
    local previous_pe_build
    previous_pe_build=$(cat "${previous_pe_build_file}")
    local previous_aio_version
    previous_aio_version=$(cat "${SERVER_DIR?}/puppet-agent-version.bak")
    local pkg_dir="${PACKAGE_DIR?}/${previous_pe_build}/${PLATFORM_TAG?}-${previous_aio_version//.g[0-9a-f]*/}"
    local pe_pkg_link="${PACKAGE_DIR?}/${previous_pe_build}/puppet_enterprise"

    gettext_wrapped_err "Validation failure." "!!"
    eval_gettext_display "Rolling puppet-agent back to \${previous_aio_version}."
    eval_gettext_display "Rolling pe-installer back to PE \${previous_pe_build}."
    eval_gettext_display "Rolling pe-modules back to PE \${previous_pe_build}."

    run "cp ${previous_pe_build_file} ${SERVER_DIR?}/pe_build"
    run "rm -f ${pe_pkg_link?}"
    run "ln -s ${pkg_dir?} ${pe_pkg_link?}"
    run "rm -f ${INSTALLER_BIN_DIR}/psqlinfo.json"
    set_repo_configuration "${pe_pkg_link}"
    case "${PLATFORM_PACKAGING?}" in
      rpm)
        run "yum downgrade -y puppet-agent-${previous_aio_version:?} pe-installer pe-modules"
        ;;
      zypper)
        remove_packages "pe-installer" "pe-modules"
        # --oldpackage is required to get the package downgraded on SLES
        run "zypper --non-interactive install --repo ${PE_REPO_NAME?} --oldpackage puppet-agent=${previous_aio_version:?} pe-installer pe-modules"
        ;;
      apt)
        remove_packages "pe-installer" "pe-modules"
        # --force-yes is required to get the package downgraded on Debian
        run "DEBIAN_FRONTEND=noninteractive apt-get install -y --force-yes ${APT_GET_ARGS?} puppet-agent=${previous_aio_version:?}* pe-installer pe-modules"
        ;;
    esac
  fi
}

#=== FUNCTION ================================================================
#        NAME:  save_psql_encoding_and_validation
# DESCRIPTION:  Queries psql to receive encoding and data, then writes
#               it to file. Only runs on database node.
#=============================================================================
save_psql_encoding_and_validation() {
  run "${INSTALLER_BIN_DIR}/save_psql_encoding.rb"
  local result=$?
  if [[ "${result}" != "0" ]]; then
    psql_data_error
  fi
}

#=== FUNCTION ================================================================
#        NAME: set_repo_configuration
# DESCRIPTION: Prepare platform specific package manager repository
#              configuration pointing to the given local directory.
#          $1: Absolute path to a directory with the local packages and
#              metadata we want to configure to
#=============================================================================
set_repo_configuration() {
  local pkg_dir=${1?}

  case "${PLATFORM_PACKAGING?}" in
    rpm)
      # Why would we need to create this folder? copied from old installer
      run "mkdir -p /etc/yum.repos.d"
      # Create repo file
      local repo_file="/etc/yum.repos.d/${PE_REPO_NAME?}.repo"
      run "echo '[${PE_REPO_NAME}]' > ${repo_file}"
      run "echo 'name=Puppet, Inc. PE Packages \$releasever - \$basearch' >> ${repo_file}"
      run "echo 'baseurl=file://${pkg_dir?}' >> ${repo_file}"
      run "echo 'enabled=1' >> ${repo_file}"
      run "echo 'gpgcheck=1' >> ${repo_file}"
      run "echo -e 'gpgkey=file://${PACKAGE_DIR?}/GPG-KEY-puppet-2025-04-06\n       file://${PACKAGE_DIR?}/GPG-KEY-puppet' >> ${repo_file}"
     run "yum clean all --disablerepo='*' --enablerepo=${PE_REPO_NAME?}"
      ;;
    zypper)
      # Remove old service if it exists
      if zypper service-list | grep -q "${PE_REPO_NAME?}"; then
        run "zypper service-delete ${PE_REPO_NAME?}"
      fi
      if zypper repos | grep -q "${PE_REPO_NAME?}"; then
        run "zypper removerepo ${PE_REPO_NAME?}"
      fi
      # The --type flag is deprecated on SLES 15 and will emit a
      # warning, so only include it on pre-15.
      typeflag=''
      if ((PLATFORM_RELEASE < 15)); then
        typeflag='--type=yum '
      fi
      run "zypper addrepo ${typeflag}file://'${pkg_dir?}' ${PE_REPO_NAME?}"
      run "zypper refresh --repo ${PE_REPO_NAME?} || :"
      ;;
    apt)
      run "mkdir -p /etc/apt/sources.list.d"
      run "echo 'deb file:${pkg_dir?} ./' > /etc/apt/sources.list.d/${PE_REPO_NAME?}.list"
      run "apt-get update -q -y"
      ;;
  esac
}

#=== FUNCTION ================================================================
#        NAME:  setup_installer_runtime
# DESCRIPTION:  Installs installer runtime by configuring a local package
#               repository and calling the respective install commands.
#=============================================================================
setup_installer_runtime() {
  local base_pkg_dir="${PACKAGE_DIR?}/${PE_BUILD_VERSION?}"
  local pkg_dir="${base_pkg_dir?}/${PLATFORM_TAG?}-${AIO_AGENT_VERSION?}"
  local pe_pkg_link="${base_pkg_dir?}/puppet_enterprise"
  local installer_pkg_path="${INSTALLER_DIR?}/packages"
  local installer_legacy_gpg_key_path="${installer_pkg_path?}/GPG-KEY-puppet"
  local installer_gpg_key_path="${installer_pkg_path?}/GPG-KEY-puppet-2025-04-06"
  local global_hiera_yaml_path="/etc/puppetlabs/puppet/hiera.yaml"

  run "mkdir -p ${pkg_dir?}"
  run "cp -r -L ${installer_pkg_path?}/${PLATFORM_TAG?}/* ${pkg_dir?}"
  run "rm -f ${pe_pkg_link?}"
  run "ln -s ${pkg_dir?} ${pe_pkg_link?}"
  run "cp -r -L ${installer_pkg_path?}/GPG-KEY-puppet* ${PACKAGE_DIR?}"

  # Install the default PE global hiera.yaml file on new installs, skip on upgrades
  if [[ -z $(current_pe_version) ]]; then
      run "mkdir -p $(dirname ${global_hiera_yaml_path})"
      run "cp conf.d/global_hiera.yaml ${global_hiera_yaml_path}"
  fi

  # Add keys
  case "${PLATFORM_PACKAGING?}" in
    rpm|zypper)
      run "rpm --import ${installer_legacy_gpg_key_path?}"
      run "rpm --import ${installer_gpg_key_path?}"
      ;;
    apt)
      run "APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add ${installer_legacy_gpg_key_path?}"
      run "APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add ${installer_gpg_key_path?}"
      ;;
  esac

  set_repo_configuration "${pe_pkg_link?}"
  if feature_flag 'pe_modules_next'; then
    # Uninstall pe-modules so that it doesn't conflict with
    # pe-modules-next
    remove_packages "pe-modules"
  fi
  install_packages "pe-installer" "${PE_MODULES_NAME:?}"
}

#=== FUNCTION ================================================================
#        NAME: upgrading
# DESCRIPTION: Return 0 if we detect a pre-existing version of PE that
#              does not match the current version being installed.
#          $1: The current pe build we are to test against.
#=============================================================================
upgrading() {
  if [ -n "${CURRENT_PE_BUILD}" ] &&
     [ "${CURRENT_PE_BUILD}" != "${NEW_PE_BUILD?}" ]; then
    return 0
  else
    return 1
  fi
}

#=== FUNCTION ================================================================
#        NAME:  wait_for_agent_lock
# DESCRIPTION:  Waits for an agent lock to clear, up to 3 minutes. Returns
#               0 if successful, 1 on a timeout.
#=============================================================================
wait_for_agent_lock() {
  local count=0
  local lockfile
  lockfile=$(puppet config print agent_catalog_run_lockfile --log_level=err)
  while [ -f "${lockfile}" ] && [ "${count?}" -le 35 ]; do
    gettext_display '* waiting for an agent run to complete...'
    sleep 5
    local count=$((count + 1))
  done
  if [ "${count?}" -gt 35 ]; then
    return 1
  else
    return 0
  fi
}
#===============================================================================

#===[ Validation Functions ]====================================================
#=== FUNCTION ================================================================
#        NAME: assert_on_agent_downgrade
# DESCRIPTION: Determine if the new agent version is a downgrade and exit
#              if it is.
#          $1: Agent version we're attempting to upgrade from.
#=============================================================================
assert_on_agent_downgrade() {
  local agent_version=$1

  # Compare to AIO_INSTALL_VERSION from bootstrap-metadata to determine
  # if we are upgrade.
  if ! is_version_less_than "${agent_version}" "${AIO_INSTALL_VERSION}" "0000"; then
    eval_gettext_err " Agent downgrades are not supported. Attempted to downgrade from Agent \${agent_version} to Agent \${AIO_INSTALL_VERSION}."
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: assert_on_agent_version_set
# DESCRIPTION: Determine if pe_repo::platform::PLATFORM_TAG::agent_version is set and
#              exit if it is mismatched to the upgrade version.
#=============================================================================
assert_on_agent_version_set() {
  pe_repo_platform_tag="${PLATFORM_TAG//-/_}"
  hiera_agent_version=$(run_with_output "${INSTALLER_BIN_DIR}/get_hiera_lookup_value_by_key.rb pe_repo::platform::${pe_repo_platform_tag}::agent_version")
  hiera_check=$?
  classifier_agent_version=$(run_with_output "${INSTALLER_BIN_DIR}/get_classifier_value.rb 'PE Master' pe_repo::platform::${pe_repo_platform_tag} agent_version")
  classifier_check=$?
  manual_check=0
  if [ "${hiera_check}" != "0" ]; then
    hiera_agent_version=''
    manual_check=1
  fi
  if [ "${classifier_check}" != "0" ]; then
    classifier_agent_version=''
    manual_check=1
  fi

  pe_repo_agent_version="${classifier_agent_version:-$hiera_agent_version}"
  upgrade_agent_version_simple=$(simplify_agent_version "${AIO_AGENT_VERSION}")
  # Check if pe_repo has a pinned version and exit if version does not match new upgrade agent version
  if [ -n "${pe_repo_agent_version}" ] && [ "${pe_repo_agent_version}" != "${upgrade_agent_version_simple}" ] ; then
    eval_gettext_display "[WARNING]: Your Hiera or classifier data includes a value for pe_repo::platform::${pe_repo_platform_tag}::agent_version: ${pe_repo_agent_version}. This could cause the upgrade to fail. Remove the agent_version parameter for this pe_repo class from the PE Master node group, pe.conf, and any other locations in Hiera."
    confirm_proceed_with_upgrade 1
  elif [ "${manual_check}" == "1" ]; then
    eval_gettext_display "[NOTE]: Verify that your Hiera and classifier data do not include any values for pe_repo::platform::${pe_repo_platform_tag}::agent_version, because this could cause the upgrade to fail."
    confirm_proceed_with_upgrade
  fi
}

#=== FUNCTION ================================================================
#        NAME: confirm_proceed_with_upgrade
# DESCRIPTION: Ask user if they want to proceed with the upgrade. If passed
#              any argument, defaults to N instead of Y.
#=============================================================================
confirm_proceed_with_upgrade() {
  if is_interactive_mode && ! eval_gettext_user_confirmation "Proceed with the upgrade?" "${1}"; then
    rollback_upgrade
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: assert_on_build_downgrade
# DESCRIPTION: Determine if the new build version is a downgrade and exit
#              if it is.
#          $1: Build version we're attempting to upgrade from.
#=============================================================================
assert_on_build_downgrade() {
  local build_version=$1

  # Compare to PE_BUILD_VERSION from bootstrap-metadata to determine
  # if we are upgrade.
  if ! is_version_less_than "${build_version}" "${NEW_PE_BUILD}"; then
    eval_gettext_err " Build downgrades are not supported. Attempted to downgrade from PE \${build_version} to PE \${NEW_PE_BUILD}."
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: assert_on_ineffective_build
# DESCRIPTION: Determine if the build version is older than our current version
#              and exit if it is.
#          $1: Build version we're attempting to upgrade from.
#=============================================================================
assert_on_ineffective_build() {
  local installed_effective_date=$1
  local effective_date=$2

  # All indications are that this will never happen. This is a sanity check just in
  # case a customer actually upgrades by version but is a downgrade by release date.
  if ! is_version_less_than "${installed_effective_date}" "${effective_date}" ; then
      eval_gettext_err "Sorry McFly, you can't upgrade to a version that was released before your current version, chronologically, even if it's part of a newer release line."
      eval_gettext_err "Release date for your current version: \${installed_effective_date}"
      eval_gettext_err "Release date for the version you're attempting to upgrade to: \${effective_date}."
    exit 1
  else
    return 0
  fi
}

#=== FUNCTION ================================================================
#        NAME: assert_on_legacy_version
# DESCRIPTION: Determine if the installed version is 3.x and exit
#              if it is.
#     CAUTION: DO NOT REMOVE THIS. Upgrding form 3.x is "really bad" and
#              this check needs to remain in place until 3.x no longer
#              exists in the wild.
#=============================================================================
assert_on_legacy_version() {
  # Disallow upgrades from 3.x
  if [[ -f "${OLD_OPT_DIR}/pe_version" ]]; then
    installed_version=$(cat "${OLD_OPT_DIR}/pe_version")
    eval_gettext_err " Upgrades from PE \${installed_version} are not supported."
    gettext_err " Review the 2016.2 migration documentation for instructions on moving to this version of PE."
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: assert_on_unsupported_version
# DESCRIPTION: Determine if the build version is unsupported and exit
#              if it is.
#          $1: Build version we're attempting to upgrade from.
#=============================================================================
assert_on_unsupported_version() {
  local build_version=$1

  # Testing for empty string because on first install, there will not be
  # an installed version.
  assert_on_ineffective_build "${INSTALLED_EFFECTIVE_DATE}" "${EFFECTIVE_DATE}"

  if [ -n "${build_version}" ] &&
      ! is_version_less_than "${MINIMUM_SUPPORTED_VERSION}" "${build_version}" ; then
    eval_gettext_err " Upgrades from PE \${build_version} are not supported."
    exit 1
  else
    return 0
  fi
}

#=== FUNCTION ================================================================
#        NAME:  ensure_database_updates_first
# DESCRIPTION:  Checks to ensure that if we are upgrading an external database
#               setup, the postgres node is updated first.
#=============================================================================
ensure_database_updates_first() {
  # Skip if force is set
  if [[ "${POSITIONAL_ARGS[*]}" =~ "--force" ]]; then
    gettext_display_announcement "Skipping database version check because --force is set"
    return 0
  fi
  is_external_postgres
  local external=$?
  if [[ "${external}" = "111" ]]; then
    gettext_display "No external postgres found."
  elif [[ "${external}" = "0" ]]; then
    run "${INSTALLER_BIN_DIR}/does_postgres_need_updating.rb"
    local updated=$?
    if [[ "${updated}" = "111" ]]; then
      gettext_display "External database already up to date."
    elif [[ "${updated}" = "0" ]]; then
      gettext_err "We detected that you have a standalone PE-PostgreSQL node which is not yet upgraded. You must upgrade the PE-PostgreSQL node before upgrading your primary server node. Rolling back upgrade now."
      if is_interactive_mode; then
        gettext_display "Press enter to roll back the upgrade"
        read -r
      fi
      rollback_upgrade
      exit 1
    else
      psql_data_error
    fi
  else
    psql_data_error
  fi
}

#=== FUNCTION ================================================================
#        NAME: pre_install_validation
# DESCRIPTION: Check the system for conditions which might cause an
#              install to fail and exit early if a condition is met.
#=============================================================================
pre_install_validation() {
  if [[ "${LOCAL_PLATFORM_TAG}" != "${PLATFORM_TAG}" ]]; then
    eval_gettext_err "We have detected that you are attempting to use a \${PLATFORM_TAG} PE tarball on \${LOCAL_PLATFORM_TAG}."
    gettext_err "Please verify you have downloaded the correct installer for your platform."
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: pre_upgrade_validation
# DESCRIPTION: Check the system for conditions which might cause an
#              upgrade to fail and exit early if a condition is met.
#=============================================================================
pre_upgrade_validation() {
  # Disallow upgrades from 3.x
  assert_on_legacy_version

  if ! upgrading ; then
    return 0 # No 2015+ install means we are not upgrading
  fi

  # The following locations hold version strings for the different Puppet Enterprise builds,
  # current version and to be installed version, and can be used for comparisons.
  #  - PE_BUILD_VERSION (copied to NEW_PE_BUILD for clarity)
  #  - current_pe_version
  assert_on_unsupported_version "${CURRENT_PE_BUILD}"
  assert_on_build_downgrade "${CURRENT_PE_BUILD}"

  # The following locations hold version strings for the different Puppet Enterprise Agent builds,
  # current version and to be installed version, and can be used for comparisons.
  #  - AIO_INSTALL_VERSION
  #  - CURRENT_PE_AGENT_VERSION
  assert_on_agent_downgrade "${CURRENT_PE_AGENT_VERSION}"

  display_new_line
  preupgrade_health_check
}

#=== FUNCTION ================================================================
#        NAME: split_deprecation_notification
# DESCRIPTION: Checks if a split install is defined in pe.conf, and if so, notify
#              that this configuration is no longer supported and exit installer.
#=============================================================================
split_deprecation_notification() {
  display_new_line
  run "${INSTALLER_BIN_DIR}/legacy_split_check.rb ${CONF_FILE_PATH}"

  local result=$?
  if [[ "${result}" == "1" ]]; then
    gettext_err "Split installation configuration is unsupported"
    if [[ -n "${CURRENT_PE_VERSION}" ]]; then
      eval_gettext_err "We noticed that Puppet Server, console, and PuppetDB are installed on separate hosts. This split installation configuration is no longer supported. See the latest PE documentation at ${LINK_SPLIT_MIGRATION} for details on migrating to a monolithic installation. After migrating to a monolithic installation, please run the installer again."
    else
      gettext_err "We noticed you are using seperate hosts for Puppet Server, console and PuppetDB in your pe.conf. This split install configuration is no longer supported. Please update your pe.conf to use only puppet_enterprise::puppet_master_host and run the installer again."
    fi
    rollback_upgrade
    exit 1
  fi
}

#=== FUNCTION ================================================================
#        NAME: validate_pe_conf
# DESCRIPTION: Basic sanity check for the pe.conf file before we attempt to use it
#=============================================================================
validate_pe_conf() {
  # Try loading the conf file with Ruby HOCON
  display_new_line
  eval_gettext_display_announcement "We're checking if \${CONF_FILE_PATH} contains valid HOCON syntax..."
  run "${INSTALLER_BIN_DIR}/validate_pe_conf.rb ${CONF_FILE_PATH}"

  local result=$?
  if [[ "${result}" != "0" ]]; then
    rollback_upgrade
    eval_gettext_err "Your pe.conf file at \${CONF_FILE_PATH} contains errors."
    gettext_err "Check your pe.conf file for errors and try re-running the installer."
    exit 1
  fi
}
#===============================================================================

#=== FUNCTION ================================================================
#        NAME: verify_secondary_connected_to_primary_broker
# DESCRIPTION: Because upgrading secondary nodes will fail if their pxp-agent
#              is not directly connected to the primary's broker, and a user will
#              be unable to fix the configuration after the primary is upgraded
#              and the secondary node will be unable to get a catalog, this
#              checks puppet infra status to ensure there are no alerts in
#              the orchestrator status.
#=============================================================================
verify_secondary_connected_to_primary_broker() {
  display_new_line
  if [ -e "/etc/puppetlabs/client-tools/services.conf" ]; then
    gettext_display_announcement "Verifying pxp-agent on secondary infrastructure nodes are connected directly to the primary server's broker..."
    run "${INSTALLER_BIN_DIR}/verify_secondary_connected_to_primary_broker.rb ${RBAC_TOKEN_PATH}"
    result=$?
    if [[ "${result}" == "0" ]]; then
      gettext_display "Verified."
    elif [[ "${result}" == "1" ]]; then
      if is_interactive_mode; then
        errormsg="Error verifying pxp-agent status. If you are sure your configuration is correct, you may bypass this check with the -y flag. Cancelling upgrade."
        eval_gettext_err "\${errormsg}"
        rollback_upgrade
        eval_gettext_err "\${errormsg}"
        exit 1
      else
        gettext_err "Error verifying pxp-agent status. Skipping check due to the presence of -y flag."
      fi
    elif [[ "${result}" == "4" ]]; then
      if is_interactive_mode; then
        gettext_display "The version of Puppet Enterprise you are upgrading from requires an RBAC token to check status."
        get_rbac_token
        verify_secondary_connected_to_primary_broker
      else
        gettext_err "Unable to check pxp-agent status without a valid RBAC token, but you are running in non-interactive mode (-y). Proceeding with upgrade."
      fi
    elif [[ "${result}" == "5" ]]; then
      errormsg="The path to the RBAC token provided (${RBAC_TOKEN_PATH}) does not appear to contain a valid token. Cancelling upgrade."
      eval_gettext_err "\${errormsg}"
      rollback_upgrade
      eval_gettext_err "\${errormsg}"
      exit 1
    elif [[ "${result}" == "6" ]]; then
      if is_interactive_mode; then
        gettext_err "Your current RBAC token has expired. Please generate a new RBAC token."
        get_rbac_token
        verify_secondary_connected_to_primary_broker
      else
        gettext_err "Unable to check pxp-agent status without an unexpired RBAC token, but you are running in non-interactive mode (-y). Proceeding with upgrade."
      fi
    elif [[ "${result}" == "7" ]]; then
      # shellcheck disable=SC2034
      errorprefix="The list of nodes printed above are"
      # shellcheck disable=SC2034
      errormsg="secondary infrastructure nodes that are not connected to the primary server's PCP broker. This will cause problems upgrading the nodes. The PE Infrastructure Agent node group's pcp_broker_list, server_list, and primary_uris parameters must include only the fully qualified domain names and relevant ports for the primary server node and replicas. Confirm this, run puppet agent -t on all nodes classified into this node group, and then try the upgrade again. See the \"Installing Compilers\" page of the PE documentation for details."
      eval_gettext_err "\${errorprefix} \${errormsg}"
      rollback_upgrade
      # shellcheck disable=SC2034
      errorprefix="Please see the error message printed prior to the roll back for a list of"
      eval_gettext_err "\${errorprefix} \${errormsg}"
      exit 1
    fi
  fi
}

#=== FUNCTION ================================================================
#        NAME: get_rbac_token
# DESCRIPTION: Prompt the user to either enter the path to an existing token
#              or to generate a new one
#=============================================================================
get_rbac_token(){
  # shellcheck disable=SC2006
  translation="`gettext \"Path to save RBAC token (this will overwrite any current token at this path)\"`"
  echo -en "${translation} [~/.puppetlabs/token]: "
  read -r RBAC_TOKEN_PATH
  if [ -z "${RBAC_TOKEN_PATH}" ]; then
    RBAC_TOKEN_PATH="${HOME}/.puppetlabs/token"
  fi
  run "/opt/puppetlabs/bin/puppet access login --lifetime 15m --token-file ${RBAC_TOKEN_PATH}"
}

#===[ Upgrade/Setup Warnings ]==================================================
#=== FUNCTION ================================================================
#        NAME: database_size
# DESCRIPTION: Check the current size of the PostgreSQL database
#=============================================================================
database_size() {
  pg_version=$(${PUPPET_BIN_DIR?}/facter -p "pe_postgresql_info.installed_server_version")
  used_bytes=$(${PUPPET_BIN_DIR?}/facter -p "pe_postgresql_info.versions.\"${pg_version}\".used_bytes")

  # Currently, the two Ruby calls below cannot be replaced with a pe_installer snippet because
  #  the pe-installer package is installed after this check, and so the necessary code snippet
  #  will not be available at the time of this check
  used_gigs=$("${PUPPET_BIN_DIR?}"/ruby -e "puts \"${used_bytes}\".to_f/1e+9")
  round_gigs=$("${PUPPET_BIN_DIR?}"/ruby -e "puts \"${used_gigs}\".to_f.round")
  echo "${round_gigs}"
}

#=== FUNCTION ================================================================
#        NAME: display_upgrade_cautions
# DESCRIPTION: Shows information about the most important cautions when
#              when upgrading, and directs the user to read the docs.
#=============================================================================
display_upgrade_cautions() {
  if upgrading; then
    display_new_line
    gettext_display "## Upgrade Cautions"
    if is_version_less_than "${CURRENT_PE_VERSION}" "2021.5.99"; then
      gigs=$(database_size)
      pg_upgrade_warning "${gigs}"
    else
      display_new_line
    fi
    gettext_display "It is highly recommended to take a backup of your PE install before doing an upgrade so that you can safely roll back if the upgrade fails."
    display_new_line
    gettext_display "If using the puppet_agent module, ensure you are using the latest version, as it may include important bug fixes which improve the upgrade experience."
    display_new_line
    gettext_display "Please read ${LINK_UPGRADE_CAUTIONS} before upgrading to PE ${NEW_PE_VERSION}."
  fi
}

#=== FUNCTION ================================================================
#        NAME: migration_runtime_calculation
# DESCRIPTION: Check current and upgrade versions and warn the user about
#              potential long-running PuppetDB migrations. This function is
#              called with the time it takes to migrate 10 GB of data. This
#              time estimate is based on calculations made in internal testing
#=============================================================================
migration_runtime_calculation() {
  base_install_time=5
  minutes_per_ten_gigs="$1"
  gigs=$2
  if [ -z "$2" ]; then
    gigs=$(database_size)
  fi

  time_estimate=$((gigs*minutes_per_ten_gigs/10+base_install_time))
  echo "${time_estimate}"
}

#=== FUNCTION ================================================================
#        NAME: pg_upgrade_warning
# DESCRIPTION: If upgrading from 11 to 14, notify the user they will need
#              additional disk space. Returns 0 if a warning was displayed.
#=============================================================================
pg_upgrade_warning() {
  if is_version_less_than "${CURRENT_PE_VERSION}" "2021.5.99"; then
    local gigs=$1
    if [ -z "$1" ]; then
      gigs=$(database_size)
    fi
    margin=$("${PUPPET_BIN_DIR}"/ruby -e "puts (\"${gigs}\".to_f*1.1).round")
    eval_gettext_display "PostgreSQL will be upgraded from version 11 to 14."
    if [[ "${margin}" != "0" ]]; then
      eval_gettext_display "Please ensure that you have at least ${margin} gigabytes of free disk space in order to complete the upgrade."
    fi
    display_new_line
    return 0
  fi
  return 1
}

#=== FUNCTION ================================================================
#        NAME:  preupgrade_health_check
# DESCRIPTION:  Validates the state of the primary, based on support's
#               preupgrade_check module
#=============================================================================
preupgrade_health_check() {
  gettext_display "## Pre-Upgrade Checks"
  display_new_line

  # Gather filesystem usage information.
  puppetdir_usage=$(df "$(${PUPPET_BIN_DIR?}/puppet config print libdir --log_level=err)" | awk '{print $5}' | grep -v Use% | cut -d '%' -f 1)
  codedir_usage=$(df "$(${PUPPET_BIN_DIR?}/puppet config print codedir --log_level=err)" | awk '{print $5}' | grep -v Use% | cut -d '%' -f 1)
  if [ "$codedir_usage" -gt "80" ]; then
    echo "Warning: Code directory filesystem utilization of ${codedir_usage}% is greater than 80% utilized."
  fi

  if [ "$puppetdir_usage" -gt "80" ]; then
    echo "Warning: Puppet directory filesystem utilization of ${puppetdir_usage}% is greater than 80% utilized."
  fi

  # Gather Puppet and PXP agent statuses.
  agent_status=$(${PUPPET_BIN_DIR?}/puppet resource service puppet | grep ensure | cut -d "'" -f2)
  pxp_status=$(${PUPPET_BIN_DIR?}/puppet resource service pxp-agent | grep ensure | cut -d "'" -f2)

  if [ "$agent_status" != "running" ]; then
    echo "Warning: Puppet agent is not running."
  fi

  if [ "$pxp_status" != "running" ]; then
    echo "Warning: PXP agent is not running."
  fi

  # If services.conf exists, we can run `puppet infra status`
  if [ -e "/etc/puppetlabs/client-tools/services.conf" ]; then
    infra_status_output=$(/opt/puppetlabs/bin/puppet infrastructure status --log_level=err)
    ruby_all_services_ok_check=$(cat <<-END
        output='${infra_status_output}'
        service_status = output.scan(%r{[0-9]+ of [0-9]+ services are fully operational})[0]
        services = service_status.nil? ?
          [] :
          service_status.scan(%r{[0-9]+})
        services_up = services[0]
        services_total = services[1]

        if services_up == services_total && services_up != 0
          puts 0
        else
          puts 1
        end
END
)
    all_services_ok=$("${PUPPET_BIN_DIR?}"/ruby -e "${ruby_all_services_ok_check}")
    if [ "${all_services_ok}" -eq 1 ]; then
      echo "Warning: \`puppet infrastructure status\` indicated not all services are fully operational"
    fi
  fi

  current_date=$(date --iso-8601)
  # Gather license information if the license file exists.
  puppet_confdir=$(${PUPPET_BIN_DIR?}/puppet config print confdir --log_level=err)
  if [ -f "${puppet_confdir}/../license.key" ]; then
    license_end=$(grep end "${puppet_confdir}"/../license.key | cut -d ':' -f 2 | tr -d '[:space:]')
    if [[ "${license_end}" < "${current_date}" ]]; then
      echo "Warning: Your PE license expired on ${license_end}, please contact Puppet to renew your license"
    fi
  fi

  # Check CA cert expiration date.
  puppet_cacert=$(${PUPPET_BIN_DIR?}/puppet config print cacert --log_level=err)
  if [ -e "${puppet_cacert}" ]; then
    cacert_enddate_long=$(${PUPPET_BIN_DIR?}/openssl x509 -enddate -noout -in "${puppet_cacert}" 2>/dev/null | grep notAfter | cut -d '=' -f2)
    cacert_enddate_short=$(date --date="${cacert_enddate_long}" --utc +"%Y-%m-%d")
    if [[ "${cacert_enddate_short}" < "${current_date}" ]]; then
      echo "Warning: Your CA certificate has expired"
    else
      cacert_enddate_sec=$(date -d "${cacert_enddate_short}" +%s)
      current_date_sec=$(date -d "$current_date" +%s)
      date_diff_seconds=$(( cacert_enddate_sec-current_date_sec ))
      date_diff_days=$(( date_diff_seconds/(60*60*24) ))
      echo "Your CA certificate will expire in ${date_diff_days} days on ${cacert_enddate_short}"
    fi
  fi

  # Check for any agents running Puppet 3.x
  puppet_localcacert=$(${PUPPET_BIN_DIR?}/puppet config print localcacert)
  puppet_hostcert=$(${PUPPET_BIN_DIR?}/puppet config print hostcert)
  puppet_hostprivkey=$(${PUPPET_BIN_DIR?}/puppet config print hostprivkey)

  puppet_3_agents=$(/opt/puppetlabs/bin/puppet-query --cacert "${puppet_localcacert}" --key "${puppet_hostprivkey}" --cert "${puppet_hostcert}" 'inventory [certname, facts.puppetversion] { facts.puppetversion ~ "^3\\..*" }' 2>/dev/null)

  # Disabling shellcheck for using $? vs. the actual command
  # shellcheck disable=SC2181
  if [ $? -eq 0 ]; then
    if [ "${puppet_3_agents}" != "[]" ]; then
      puppet_3_agent_list=$(cat <<-END
        require 'json';
        begin
          agent_list = JSON.parse('${puppet_3_agents}')
          puts agent_list.map { |agent| agent['certname'] }
        rescue JSON::ParserError
          puts "failed"
        end
END
)
      agent_list=$("${PUPPET_BIN_DIR?}"/ruby -e "${puppet_3_agent_list}")
      local result=$?
      echo "Your Puppet 3 agents aren't compatible with your target PE version. Upgrade Puppet 3 agents before upgrading PE. For best compatibility, we recommend Puppet agent 6 or later."

      if [ "$agent_list" != "failed" ] && [ $result -eq 0 ]; then
        echo "The following agents appear to be running Puppet 3:"
        echo "${agent_list}"
      fi
    fi
  fi

}
#===============================================================================

#===[ pe.conf Setup Functions ]=================================================
#=== FUNCTION ================================================================
#        NAME: prepare_express_pe_conf
# DESCRIPTION: Create a custom-pe.conf from the default pe.conf dropping the console_admin_password field
#=============================================================================
prepare_express_pe_conf() {
  local custom_pe_conf_file="${INSTALLER_DIR?}/conf.d/custom-pe.conf"
  CONF_FILE_PATH=${custom_pe_conf_file?}
  run "cp ${DEFAULT_CONF_FILE_PATH?} ${CONF_FILE_PATH?}" > /dev/null
}

#=== FUNCTION ================================================================
#        NAME: use_existing_pe_conf
# DESCRIPTION: Set the conf file path to the pre-existing pe.conf.
#=============================================================================
use_existing_pe_conf() {
  eval_gettext_display "Your pe.conf file was found at \${ENTERPRISE_CONF_PATH}."
  CONF_FILE_PATH=${ENTERPRISE_CONF_PATH?}
}
#===============================================================================

#===[ Installer Workflow ]======================================================
#=== FUNCTION ================================================================
#        NAME:  main
# DESCRIPTION:  Controls program flow.
#=============================================================================
main() {
  #----------------------------------------------------------------------------
  # Check if we are running as root first
  #----------------------------------------------------------------------------
  if [[ "$(id -u)" -ne 0 ]]; then
    eval_gettext_err "\${SCRIPT_NAME} must be run as root"
    exit 1
  fi

  #----------------------------------------------------------------------------
  # Parse the input for flags
  #----------------------------------------------------------------------------
  while getopts c:DhqVypns name; do
      case "$name" in
          c)
              CONF_FILE_PATH="$(readlink -f "${OPTARG?}")"
              ;;
          D)
              IS_DEBUG='true'
              ;;
          h)
              print_usage
              exit 0
              ;;
          q)
              IS_SUPPRESS_OUTPUT='true'
              ;;
          V)
              set -x
              ;;
          y)
              IS_INTERACTIVE_MODE='false'
              ;;
          p)
              IS_PREP_ONLY='true'
              ;;
          s)
	      IS_SKIPPING_DB_CHECK='true'
	      ;;
          ?)
              print_usage
              exit 1
              ;;
      esac
  done

  POSITIONAL_ARGS="${*:$OPTIND:$#}"

  # If a config file is specified, assume the user knows what they are doing
  # This behavior is similar to the old installer.
  if [[ -n "${CONF_FILE_PATH}" ]]; then
    IS_INTERACTIVE_MODE='false'

    # Check that the conf actually exists
    if [[ ! -f "${CONF_FILE_PATH}" ]]; then
      eval_gettext_err "We couldn't find a PE config file at \${CONF_FILE_PATH}."
      gettext_err "Verify that a pe.conf exists at the path you provided."
      exit 1
    fi
  fi

  #-------------------------------------------------------------------------
  # Input validation
  #-------------------------------------------------------------------------
  readonly IS_DEBUG
  readonly IS_INTERACTIVE_MODE
  readonly IS_SUPPRESS_OUTPUT
  readonly IS_PREP_ONLY
  readonly IS_SKIPPING_DB_CHECK

  if is_quiet_mode; then
    # suppress output from the install process
    exec > /dev/null 2>&1
  fi

  #-------------------------------------------------------------------------
  # High-level installer logic
  #-------------------------------------------------------------------------

  pushd "${INSTALLER_DIR?}" > /dev/null || exit 1

  CURRENT_PE_VERSION=$(current_pe_version)
  CURRENT_PE_BUILD=$(current_pe_build)
  CURRENT_PE_AGENT_VERSION=$(current_pe_agent_version)
  NEW_PE_BUILD=$(new_pe_build)
  NEW_PE_VERSION=$(new_pe_version)

  prepare_logging

  backup_previous_build_info
  detect_platform

  pre_install_validation

  show_welcome_message

  # install_method_str is used to set the INSTALL_METHOD variable
  # if it doesn't already exist. This will be passed to the puppet infra configure
  # action via the --install-method flag for analytics. If the user passes in a pe.conf
  # via -c, the big if statement below is skipped, so this is the default value.
  install_method_str="-c_pe_conf"

  # If user did not pass in pe.conf via -c
  if [[ -z "${CONF_FILE_PATH}" ]]; then
    if is_interactive_mode; then
      if [[ -f "${ENTERPRISE_CONF_PATH}" ]]; then
        # Didn't pass in -c but pe.conf exists
        # Either the user is upgrading, in which case the configure action will recognize this and ignore
        # the INSTALL_METHOD value, or the user is repairing.
        install_method=1
        install_method_str="repair"
        use_existing_pe_conf
      else
        install_method=0
        install_method_str="express"
        prepare_express_pe_conf
      fi
    elif [[ -f "${ENTERPRISE_CONF_PATH}" ]]; then
      # Non-interactive and pe.conf exists
      # Either the user is upgrading, in which case the configure action will recognize this and ignore
      # the INSTALL_METHOD value, or the user is repairing.
      install_method=1
      install_method_str="repair"
      use_existing_pe_conf
    else
      eval_gettext_err "The -y flag cannot be used without the -c flag if a valid pe.conf cannot be found at \${ENTERPRISE_CONF_PATH}."
      exit 1
    fi
  fi
  INSTALL_METHOD="${INSTALL_METHOD:-$install_method_str}"

  pre_upgrade_validation
  display_upgrade_cautions
  display_analytics_warning

  # Confirm that the user would actually like to install using the pe.conf file
  # located at ${CONF_FILE_PATH}
  if is_interactive_mode && ! confirm_proceed_with_install_or_upgrade ${install_method}; then
    exit 1
  fi

  if feature_flag 'pe_modules_next'; then
    PE_MODULES_NAME='pe-modules-next'
  fi

  do_install
  popd || exit 1
}

#=== FUNCTION ================================================================
#        NAME: do_install
# DESCRIPTION: Proceed with an install based on a config file.
#=============================================================================
do_install() {
    disable_puppet_agent
    setup_installer_runtime
    # Sanity check on pe.conf file provided
    validate_pe_conf
    split_deprecation_notification
    # In case of an upgrade, do split check at the beginning and save psql data
    # PE-30309 Ensure agent_version is not set in pe_repo
    if upgrading ; then
      if [ "${IS_SKIPPING_DB_CHECK?}" = "false" ] ; then
        ensure_database_updates_first
      fi
      # If external db and not on primary, or if true mono and on primary, save encoding
      if is_db_node ; then
        if [ "${IS_SKIPPING_DB_CHECK?}" = "false" ] ; then
          save_psql_encoding_and_validation
        fi
        if ! is_external_postgres; then
          assert_on_agent_version_set
        fi
      else
        assert_on_agent_version_set
      fi
    fi
    install_puppet_agent

    if upgrading ; then
      ensure_agent_upgraded
      verify_secondary_connected_to_primary_broker
    fi

    move_win64_msi

    clean_reset_url_file
    install_pe
}

install_pe() {
  eval_gettext_display_announcement "We're configuring PE using \${CONF_FILE_PATH}..."

  # Copy the effective date into place.
  echo "${EFFECTIVE_DATE}" > "${BUILD_DATE_FILE}"

  # Copy the pe_build version file into place
  run "mkdir -p ${SERVER_DIR?}"
  run "cp -L ${INSTALLER_DIR?}/VERSION ${SERVER_DIR?}/pe_build"
  run "chown root:root ${SERVER_DIR?}/pe_build"
  run "chmod 644 ${SERVER_DIR?}/pe_build"

  # Copy the hiera.yaml and conf file into place
  CONF_FILE_DEST="${ENTERPRISE_CONF_DIR?}/pe.conf"
  if [ -f "${CONF_FILE_DEST?}" ]; then
    run "cp ${CONF_FILE_DEST?} ${ENTERPRISE_CONF_DIR?}/pe-$(get_unix_timestamp).conf"
  fi
  if [ "${CONF_FILE_PATH?}" != "${CONF_FILE_DEST?}" ]; then
    run "cp ${CONF_FILE_PATH?} ${CONF_FILE_DEST?}"
  fi
  if [ -z "${CURRENT_PE_BUILD}" ]; then
    run "chown root:root ${CONF_FILE_DEST?}"
    run "chmod 600 ${CONF_FILE_DEST?}"
    # pe.conf permissions managed by puppet_enterprise::master::meep
  fi

  # Copy the uninstaller
  run "cp ${INSTALLER_DIR?}/puppet-enterprise-uninstaller /opt/puppetlabs/bin"
  run "chown root:root /opt/puppetlabs/bin/puppet-enterprise-uninstaller"
  run "chmod 755 /opt/puppetlabs/bin/puppet-enterprise-uninstaller"

  if [ "${IS_DEBUG?}" = "true" ]; then
    local debug_flag="--debug"
  else
    local debug_flag=""
  fi

  # If something fails in the shim or early in configure, or the -p flag was used,
  # pe_build will have been updated, but pe_version will not (gets laid down by
  # pe-puppet-enterprise-release). This way, we can at least try to correctly
  # apply an upgrade again.
  #
  # Note: this will not fix the issue if you are rerunning a dev-to-dev upgrade
  # (e.g. 2018.1.10-rc8 to 2018.1.10-rc9), as they will both have the same pe_version
  local upgrade_from_flag
  if upgrading ; then
    upgrade_from_flag="--upgrade-from=${CURRENT_PE_VERSION}"
  elif [ -n "${CURRENT_PE_VERSION}" ] &&
       [[ $(padded_version_string "${CURRENT_PE_VERSION}") < $(padded_version_string "${NEW_PE_BUILD?}") ]]; then
    upgrade_from_flag="--upgrade-from=${CURRENT_PE_VERSION}"
  else
    upgrade_from_flag=""
  fi

  local install_method_flag
  if [ -n "${INSTALL_METHOD}" ]; then
    install_method_flag="--install-method='${INSTALL_METHOD}'"
  else
    install_method_flag=""
  fi

  if [ "${IS_PREP_ONLY?}" = "false" ]; then

    declare -a cmd
    cmd=(
      "${PUPPET_BIN_DIR?}/puppet infrastructure configure"
      "${debug_flag?}"
      "--detailed-exitcodes"
      "--environmentpath ${ENVIRONMENTPATH?}"
      "--environment ${ENVIRONMENT?}"
      "--no-noop"
      "--libdir /dev/null"
      "--factpath /dev/null"
      "--install=${NEW_PE_VERSION?}"
      "--disable_warnings deprecations"
      "${install_method_flag?}"
      "${upgrade_from_flag?}"
      "${POSITIONAL_ARGS}"
    )

    run "${cmd[*]}"
    local result=$?
  else
    local result=0
  fi

  display "* ${cmd[*]}"
  display "* returned: ${result}"
  case "${result}" in
    0 | 2)
      if [ "${IS_SKIPPING_DB_CHECK?}" = "false" ] ; then
        # Check for presence of postgres11 and older versions, and display info message if both pg11 and older versions are present
        # Nothing will be displayed if only pg11 or only an older version is installed
        run "${INSTALLER_BIN_DIR}/old_postgresql_check.rb"
      fi
      # We can't rely on is_external_postgres and is_db_node runs from before install, since
      # they won't return the right thing on fresh installs, so we run them again here.
      # Output is redirected to /dev/null since you'll get errors when custom facts try to
      # get loaded out of cache on the node, before the primary is finished being set up.
      IS_EXT_PG=$(is_external_postgres > /dev/null; echo $?)
      IS_DB_NODE=$(is_db_node > /dev/null; echo $?)

      # Migrate any deprecated parameters set in the console that PE itself sets automatically
      # so users do not get deprecation warnings on puppet runs. Don't run on standalone
      # postgres node.
      if upgrading && [[ "${IS_PREP_ONLY?}" = "false" ]]; then
        if [[ "${IS_EXT_PG}" != "0" &&  "${IS_SKIPPING_DB_CHECK?}" = "false" ]] || [[ "${IS_EXT_PG}" = "0" && "${IS_DB_NODE}" != "0" &&  "${IS_SKIPPING_DB_CHECK?}" = "false" ]] || [[ "${IS_SKIPPING_DB_CHECK?}" = "true" && $(${PUPPET_BIN_DIR?}/puppet config print certname) == $(${PUPPET_BIN_DIR?}/puppet lookup puppet_enterprise::puppet_master_host --render-as s) ]]; then
          run "${INSTALLER_BIN_DIR}/migrate_parameters.rb"

          # Stopping the server to prune Puppet's CA CRL and restart the server.
          run "/opt/puppetlabs/bin/puppet resource service pe-puppetserver ensure=stopped"
          run "/opt/puppetlabs/bin/puppetserver ca prune"
          run "/opt/puppetlabs/bin/puppet resource service pe-puppetserver ensure=running"
        fi
      fi

      display_line_break
      gettext_display_announcement "Puppet Enterprise configuration complete!"
      eval_gettext_display "Documentation: ${LINK_USER_GUIDE}"
      eval_gettext_display "Release notes: ${LINK_RELEASE_NOTES}"

      if [ "${IS_PREP_ONLY?}" = "false" ]; then
        gettext_display_announcement "Final setup steps"
        # We are no longer showing the actual URL since we have the console_password command,
        # but this lets us know we just did an express install. We should eventually change
        # how this works so we can get rid of this file.
        if [[ -f "${RESET_URL_FILE}" ]]; then
          gettext_display "* Set the console admin password by running, as root, 'puppet infrastructure console_password --password=<MY_PASSWORD>'."
          clean_reset_url_file
        fi

        if [[ "${IS_EXT_PG}" = "0" ]] ; then
          if [[ "${IS_DB_NODE}" = "0" ]] ; then
            gettext_display "* Standalone PE-PostgreSQL node installed. Please finish installing Puppet Enterprise on your primary server node."
          else
            gettext_display "* Run 'puppet agent -t', first on your primary server node, then on your standalone PE-PostgreSQL node, and then again on your primary server node."
            ca_dir_migration
          fi
        else
          gettext_display "* Run 'puppet agent -t' twice on the primary server node."
          ca_dir_migration
        fi
      else
        display_new_line
        gettext_display "To finish configuration, run puppet-enterprise-installer again without the -p flag."
      fi
      display_new_line
      display_line_break
      ;;
    17)
      # We wait for the agent lock to clear, to prevent the agent run from attempting to
      # write to puppet_enterprise.repo at the same time as the rollback
      if wait_for_agent_lock; then
        rollback_upgrade
      fi
      gettext_wrapped_err "The installer could not acquire an agent lock." "!!"
      gettext_wrapped_err "Please ensure that the Puppet service is not running, then re-run the installer to complete the installation or upgrade." "!!"
      exit 17
      ;;
    27 | 37)
      # Configure failed before the puppet apply began
      # 27 == Validation failed
      # 37 == ha_puppetdb_sync (unsync) plan failed
      rollback_upgrade
      install_error ${result}
      ;;
    47)
      if [ -n "$upgrade_from_flag" ]; then
        # Configure failed unexpectedly before apply.
        # Only rollback if upgrading.
        rollback_upgrade
      fi
      install_error ${result}
      ;;
    *)
      install_error ${result}
      ;;
  esac
}
#===============================================================================

if [ "${SCRIPT_NAME}" == 'puppet-enterprise-installer' ]; then
    main "$@"
fi

# vim: tabstop=2:softtabstop=2:shiftwidth=2:expandtab:tw=72:fo=cqt:wm=0
