#!/bin/bash

#-------------------------------------------------------------------
# This is the automated Tableau Server setup script for use with the
# Enterprise Deployment Guide. See:
# https://help.tableau.com/current/guides/enterprise-deployment/en-
# us/edg_intro.htm
#
# Pre-requisite: Before running this script, you must configure your AWS 
# environment according to the specifications in Part 3 of the EDG. You
# must also install and prepare PostgreSQL according to specifications
# in Part 4 of the EDG.
#
# This script will set up a EDG cluster that maps to the four-node 
# Tableau Server cluster as described in Part 4 of the EDG.
#
# Copy this script to the bastion host and run it from the home directory.  
# The script assumes passwordless sudo for all access. If this is not the 
# case, then you'll be prompted for passwords ad infinitum.
#
# For more information about running this script, see the 
# Appendix of the EDG.
#-------------------------------------------------------------------

#-------------------------------------------------------------------
# usage
#
# Displays the command-line options with explanations.
#
usage() {
  cat <<EOM
$(basename "$0") utility -- installs four-node Tableau Server according to Enterprise Deployment Guide specifications.

Usage: $(basename "$0") [options]

Required Parameters
    -1,--node1-ip <ADDRESS>             IPv4 address for the node1 host.
    -2,--node2-ip <ADDRESS>             IPv4 address for the node2 host.
    -3,--node3-ip <ADDRESS>             IPv4 address for the node3 host.
    -4,--node4-ip <ADDRESS>             IPv4 address for the node4 host.
    -5,--pgsql-ip <ADDRESS>             IPv4 address for the PostgreSQL host.

    -i,--installer-url <URL>            URL of the Tableau Server installer
                                        rpm file.
    -k,--license-key <KEY>[,<KEY2>,...] Product key(s) to activate Server
                                        license(s), separated by commas.
    -r,--registration-file <FILE PATH>  Full path to the registration JSON
                                        file.  For more information, search
                                        "tsm register" at
                                        https://help.tableau.com.
    -u,--initial-username <USERNAME>    User name for the initial Tableau
                                        Server administrator account.

Optional Parameters
    -a,--use-atr                        Override the Tableau Server installer
                                        default behavior for enabling\disabling
                                        the Authorization-to-Run (ATR) service.
                                        0 = No ATR; 1 = Use ATR.
    -c,--color [0|1]                    Use colorized output.
                                        0 = No color; 1 = Color (default).
    -f,--config-file <FILE PATH>        Full path to a configuration file.
                                        Values in the configuration file
                                        supersede their corresponding command-
                                        line options.
    -g,--gen-config-file <FILE PATH>    Generates a configuration file template
                                        in the specified path. You must edit
                                        the resulting file to fill in missing
                                        values.
    -h,--help                           Show this usage information.
    -t,--pgsql-tar-file <FILE PATH>     Full path to the Step 1 tar backup file
                                        on the PostgreSQL host.
                                        If not specified, the tar backup will
                                        not be restored.
                                        If you have configured PostgreSQL
                                        according to the Enterprise Deployment
                                        Guide specifications, then the path
                                        is /var/lib/pgsql/step1.12.bkp.tar
    -L,--install-path <PATH>            Installation path for Tableau Server.
                                        Default is /app/tableau_server.
EOM
}

#-------------------------------------------------------------------
# colorprint
#
# Prints colored, bold text to the console.
# $1 - the text to echo
# $2 - the text color.  (default is white)
#
# Constants used by this function
readonly BLACK=0
readonly RED=1
readonly GREEN=2
readonly YELLOW=3
readonly BLUE=4
readonly PURPLE=5
readonly CYAN=6
readonly WHITE=7

colorprint () {
  if [[ $USE_COLOR -eq 1 ]]; then
    color=${2:-$WHITE}
    if [[ ! -z $1 ]]; then
      printf "\033[1;9%sm%s\033[0m" "$color" "${1}"
    fi
  else
      printf "\033[1m%s\033[0m" "${1}"
  fi
}

#-------------------------------------------------------------------
# error_message
#
# Displays an error message
# $1 - message to display
#
error_message() {
  local message=${1:-"Encountered an unknown error."}
  colorprint "ERROR: " $RED;
  echo "${message}"
  echo
}

#-------------------------------------------------------------------
# delete_file_from_host
#
# $1 - file name
# $2 - Host IP
#
delete_file_from_host() {
  if [[ -n $1 && -n $2 ]]; then
    ssh -q -t ec2-user@$2 "rm -f $1"
  fi
}

#-------------------------------------------------------------------
# clean_up_sensitive_files
#
# Deletes all files that may hold sensitive data.
#
clean_up_sensitive_files() {
  local failures=()

  # Local files
  if [[ -n $CONFIG_FILE && -f $CONFIG_FILE ]]; then
    if ! rm -f $CONFIG_FILE; then
      failures+=("${CONFIG_FILE}")
    fi
  fi
  if ! rm -f $BOOTSTRAP_FILE; then failures+=("$BOOTSTRAP_FILE"); fi
  if ! rm -f $PGSQL_CONFIG_FILE; then failures+=("$PGSQL_CONFIG_FILE"); fi

  # Remote files
  if ! delete_file_from_host $REGISTRATION_FILE $NODE1_IP; \
    then failures+=("${REGISTRATION_FILE} on host ${NODE1_IP}"); fi
  if ! delete_file_from_host $PGSQL_CONFIG_FILE $NODE1_IP; \
    then failures+=("${PGSQL_CONFIG_FILE} on host ${NODE1_IP}"); fi
  if ! delete_file_from_host $BOOTSTRAP_FILE $NODE1_IP; \
    then failures+=("${BOOTSTRAP_FILE} on host ${NODE1_IP}"); fi
  if ! delete_file_from_host $BOOTSTRAP_FILE $NODE2_IP; \
    then failures+=("${BOOTSTRAP_FILE} on host ${NODE2_IP}"); fi
  if ! delete_file_from_host $BOOTSTRAP_FILE $NODE3_IP; \
    then failures+=("${BOOTSTRAP_FILE} on host ${NODE3_IP}"); fi
  if ! delete_file_from_host $BOOTSTRAP_FILE $NODE4_IP; \
    then failures+=("${BOOTSTRAP_FILE} on host ${NODE4_IP}"); fi

  if (( ${#failures[*]} > 0 )); then
    colorprint "ERROR: " $RED
    colorprint "The following files could not be deleted and must be deleted manually:"
    echo
    for failure in "${failures[@]}"
    do
      echo "  ${failure}"
    done
    colorprint "Failure to delete these files poses a security risk."
    echo
    false
  fi
}

#-------------------------------------------------------------------
# run_ssh_command_exit_if_error
#
# $1 - IP address
# $2 - Command to run
# $3 - Error message
#
run_ssh_command_exit_if_error() {
  if ! ssh -t -q ec2-user@$1 "${2}"; then
    error_message "${3}"
    clean_up_sensitive_files
    exit 1
  fi
}

#-------------------------------------------------------------------
# apply_pending_changes
#
apply_pending_changes() {
  colorprint "Applying pending changes..."; echo
  run_ssh_command_exit_if_error $NODE1_IP "tsm pending-changes apply --ignore-warnings --ignore-prompt" \
"Unable to apply pending changes."
  echo
}

#-------------------------------------------------------------------
# obliterate_server_on_host
#
# $1 - IP address
#
obliterate_server_on_host() {
  colorprint  "Obliterating any existing Tableau Server on ${1}..."; echo
  local version=$(ssh -q ec2-user@$1 "echo \$TABLEAU_SERVER_DATA_DIR_VERSION")
  if [[ -n $version ]]; then
    local old_scripts_dir=\
$(ssh -q ec2-user@$1 "printenv | grep $version | tr -s ':' '\n' | sed s/PATH=// | grep -o ^/.*packages/")
    if [[ -n $old_scripts_dir ]]; then
      old_scripts_dir=${old_scripts_dir}scripts.${version}
      run_ssh_command_exit_if_error $1 "sudo ${old_scripts_dir}/tableau-server-obliterate -a -l -y -y -y" \
        "Could not obliterate Tableau Server on $1.  You must manually log into ${1} and run \
\"tableau-server-obliterate -a -l -y -y -y\"."
      echo
    fi
  else
    echo "Tableau Server not found on ${1}."
  fi
}

#-------------------------------------------------------------------
# install_TS_on_host
#
# $1 - IP address
#
install_TS_on_host() {
  colorprint "Updating ${1}..."; echo
  ssh -t -q ec2-user@$1 "sudo yum -y update"
  echo

  # Obliterate before installing
  obliterate_server_on_host $1
  echo

  local installer_rpm="${INSTALLER_URL##*/}"

  colorprint "Checking for ${installer_rpm} on ${1}..."; echo
  if ssh -t -q ec2-user@$1 "[[ -f /home/ec2-user/$installer_rpm ]]"; then
    echo "${installer_rpm} already exists on this host."
  else
    colorprint "Downloading ${installer_rpm}..."; echo
    run_ssh_command_exit_if_error $1 "wget ${INSTALLER_URL}" "Could not download installer."
  fi

  #TFSID: 1394988 - Always install dependencies, regardless of whether the installer exists on the machine.
  echo
  colorprint "Installing dependencies..."; echo
  run_ssh_command_exit_if_error $1 \
    "sudo yum deplist ${installer_rpm} | awk '/provider:/ {print $2}' | sort -u | xargs sudo yum -y install" \
    "Could not install dependencies."

  echo
  colorprint "Installing Tableau Server in ${INSTALL_PATH} on ${1}..."; echo
  run_ssh_command_exit_if_error $1 "sudo mkdir -p ${INSTALL_PATH}" "Could not create directory ${INSTALL_PATH}."
  run_ssh_command_exit_if_error $1 "sudo rpm -i --prefix ${INSTALL_PATH} ${installer_rpm}" \
    "Failed to install ${installer_rpm}.    You must manually log into ${1} and run \
\"tableau-server-obliterate -a -l -y -y -y\"."
}

#-------------------------------------------------------------------
# initialize_node
#
# $1 - IP address
#
initialize_node() {
  colorprint "Initializing "

  # Get the initialize-tsm path
  local initialize_tsm_path=$(ssh -q -t ec2-user@$1 "find ${INSTALL_PATH}/packages -type f -name initialize-tsm")
  initialize_tsm_path=${initialize_tsm_path%%[[:space:]]}
  if [[ -z $initialize_tsm_path ]]; then
    error_message "Could not initialize server on ${1}."
  fi

  local ssh_cmd="sudo ${initialize_tsm_path} -d /data/tableau_data -b /home/ec2-user/${BOOTSTRAP_FILE} \
--accepteula; rm -f /home/ec2-user/${BOOTSTRAP_FILE}"
  case "${1}" in
    $NODE1_IP)
        local ATR_OFF="--no-activation-service"
        local atr_str=""
        if [[ -n $USE_ATR ]]; then
          # ATR was specified.  Employ special logic based on the version of the installer.

          # For maintainability, call this as needed.  It's not a perf bottleneck.
          parse_installer_version

          if (( $USE_ATR == 0 )); then
            # From 2021.4 onwards, the TS installer has ATR on by default, and added the
            # --no-activation-service flag in addition to the --activation-service flag.  Note that
            # using --no-activation-service prior to 2021.4 results in an error that exits the TS installer.
            # Prior to 2021.4, the TS installer had ATR off by default, and there was only the option
            # to specify the --activation-service flag.
            if (( $MAJOR_VERSION == 2021 )); then
              if (( $MINOR_VERSION >= 4 )); then
                atr_str="$ATR_OFF"
              fi
            elif (( $MAJOR_VERSION >= 2022 )); then
              atr_str="$ATR_OFF"
            fi
          elif (( $USE_ATR == 1 )); then
            atr_str="--activation-service"
          fi
        fi
      colorprint "node1..."; echo
      ssh_cmd="sudo ${initialize_tsm_path} -d /data/tableau_data --accepteula ${atr_str}"

      ;;
    $NODE2_IP)
      colorprint "node2..."; echo
      ;;
    $NODE3_IP)
      colorprint "node3..."; echo
      ;;
    $NODE4_IP)
      colorprint "node4..."; echo
      ;;
  esac

  run_ssh_command_exit_if_error $1 "${ssh_cmd}" "Failed to initialize Tableau Server on ${1}"
}

#-------------------------------------------------------------------
# display_banner
#
# $1 - Message to display
#
display_banner() {
  colorprint "---------------------------------------------"; echo
  colorprint "   ${1}"; echo
  colorprint "---------------------------------------------"; echo
  echo
}

#-------------------------------------------------------------------
# get_pgsql_version
#
# Decides the pgsql version to use based on the file name of the TS installer.
#

get_pgsql_version() {
  # Maintainability: call this whenever it's needed.  It's not a perf issue.
  parse_installer_version

  local is_dev_version=0
  if [[ ${MAINT_REL_VERSION} == "dev" ]]; then
    is_dev_version=1
  fi

  pgsql_version=0

  if (( $MAJOR_VERSION == 2021 )); then
    # Minimum supported 2021 version is 2021.2.2
    if (( ${MINOR_VERSION} == 2 && ( $is_dev_version == 1 || ${MAINT_REL_VERSION} > 1 ) )); then
      pgsql_version="12"
    else
      if (( ${MINOR_VERSION} > 2)); then
        pgsql_version="12"
      fi
    fi
  elif (( $MAJOR_VERSION >= 2022 )); then
    # All 2022+ versions use pgsql v13
    if (( ${MINOR_VERSION} > 0 && ( $is_dev_version == 1 || ${MAINT_REL_VERSION} >= 0 ) )); then
      pgsql_version="13"
    fi
  fi

  if (( $pgsql_version == 0 )); then
    error_message "Unable to determine the PostgreSQL version."
    exit 1
  fi
}

#-------------------------------------------------------------------
# restore_pgsql_step1_tar_backup
#
# Resotres the "Step 1" tar backup from the EDG.
#
restore_pgsql_to_initial_state() {
  # If no backup file is given, then don't try to restore anything.
  if [[ -n $PGSQL_TAR_FILE ]]; then
    local pgsql_version=0
    get_pgsql_version

    local pgsql_tar_path=$(dirname "${PGSQL_TAR_FILE}")
    colorprint "Decompressing ${PGSQL_TAR_FILE} on host $PGSQL_IP..."; echo
    if ssh -q ec2-user@$PGSQL_IP "sudo ls -al ${PGSQL_TAR_FILE}" &>/dev/null; then
        if ! ssh -q -t ec2-user@$PGSQL_IP "sudo systemctl stop postgresql-${pgsql_version}; \
  sudo tar -xvf ${PGSQL_TAR_FILE} -C ${pgsql_tar_path}; sudo systemctl start postgresql-${pgsql_version}" &>/dev/null; then
          error_message "Encountered an error decompressing ${PGSQL_TAR_FILE} on host $PGSQL_IP."
        fi
    else
      error_message "Could not find ${PGSQL_TAR_FILE} on host $PGSQL_IP."
      clean_up_sensitive_files
      exit 1
    fi
    echo
  fi
}

#-------------------------------------------------------------------
# install_initial_node
#
install_initial_node() {
  display_banner "INSTALLING NODE1 on ${NODE1_IP}"

  install_TS_on_host $NODE1_IP; echo
  initialize_node $NODE1_IP; echo

  #License activation
  local keys=(`echo $LICENSE_KEY | tr ',' ' '`)
  for key in "${keys[@]}"
  do
    colorprint "Activating license ${key}..."; echo
    run_ssh_command_exit_if_error $NODE1_IP "tsm licenses activate -k ${key}" "Unable to activate license ${key}."
  done
  echo

  # Registration
  colorprint "Registering Tableau Server..."; echo

  if [[ ! -f $REGISTRATION_FILE ]]; then
    error_message "Could not find registration file ${REGISTRATION_FILE}."
    clean_up_sensitive_files
    exit 1
  fi

  if ! scp -q $REGISTRATION_FILE ec2-user@$NODE1_IP:/home/ec2-user; then
    error_message "Unable to copy registration file to ${NODE1_IP}."
    clean_up_sensitive_files
    exit 1
  fi

  run_ssh_command_exit_if_error $NODE1_IP "tsm register --file ${REGISTRATION_FILE##*/}" "Registration failed."
  # Attempt to remove the registration file from node1.
  delete_file_from_host "${REGISTRATION_FILE##*/}" $NODE1_IP
  echo

  # Configure identity store
  colorprint "Configuring identity store..."; echo
  local scripts_dir=\
$(ssh -q -t ec2-user@$NODE1_IP "find ${INSTALL_PATH}/packages -type f -name initialize-tsm | sed s/initialize-tsm//")
  scripts_dir=${scripts_dir%%[[:space:]]}
  run_ssh_command_exit_if_error $NODE1_IP "tsm settings import -f ${scripts_dir}config.json" \
    "Failed to configure identity store."
  echo

  # Configure external postgres
  colorprint "Configuring external PostgreSQL..."; echo
  echo -e \
"{\n  \"flavor\":\"generic\",\n  \"masterUsername\":\"postgres\",\n  \"masterPassword\":\"${PGSQL_PWD_UNSAFE}\",\n  \
\"host\":\"$PGSQL_IP\",\n  \"port\":5432\n}" > $PGSQL_CONFIG_FILE
  scp -q $PGSQL_CONFIG_FILE ec2-user@$NODE1_IP:/home/ec2-user
  rm -f $PGSQL_CONFIG_FILE

  run_ssh_command_exit_if_error $NODE1_IP "tsm topology external-services repository enable -f ${PGSQL_CONFIG_FILE} --no-ssl" \
    "PostgreSQL configuration failed."
  run_ssh_command_exit_if_error $NODE1_IP "rm -f ${PGSQL_CONFIG_FILE}" "Unable to delete PostgreSQL configuration file."
  echo

  apply_pending_changes

  #Initialize server
  colorprint "Initializing Tableau Server..."; echo

  run_ssh_command_exit_if_error $NODE1_IP "tsm initialize --start-server --request-timeout 1800" \
    "Server initialization failed on ${NODE1_IP}."
  echo

  # Set initial user
  colorprint "Setting initial user..."; echo
  run_ssh_command_exit_if_error $NODE1_IP \
    "tabcmd initialuser --server http://localhost --username ${INITIAL_USERNAME} --password ${INITIAL_USER_PWD_UNSAFE}" \
    "Unable to set initial Tableau Server administrator user account on ${NODE1_IP}."
  echo
}

#-------------------------------------------------------------------
# send_bootstrap_file_to_host
#
# $1 - IP address of recipient
#
send_bootstrap_file_to_host() {
  # Generate bootstrap file on node1
  colorprint "Generating bootstrap file..."; echo
  run_ssh_command_exit_if_error $NODE1_IP "tsm topology nodes get-bootstrap-file --file ${BOOTSTRAP_FILE}" \
    "Unable to generate bootstrap file."

  # Copy it to the recipient
  # Copy first to the bastion then from bastion to recipient
  if ! scp -q ec2-user@$NODE1_IP:/home/ec2-user/${BOOTSTRAP_FILE} . ; then
    error_message "Unable to copy bootstrap file from ${NODE1_IP} to the bastion host."
    clean_up_sensitive_files
    exit 1
  fi
  if ! scp -q $BOOTSTRAP_FILE ec2-user@$1:/home/ec2-user/ ; then
    error_message "Unable to copy bootstrap file from the bastion host to ${1}."
    clean_up_sensitive_files
    exit 1
  fi
}

#-------------------------------------------------------------------
# configure_node
#
# #1 - IP address of node's host
#
configure_node() {
  local ssh_cmd
  local n=2
  if [[ $1 == $NODE2_IP ]]; then
    ssh_cmd="tsm topology set-process -n node2 -pr clustercontroller -c 1; \
tsm topology set-process -n node${n} -pr gateway -c 1; \
tsm topology set-process -n node${n} -pr vizportal -c 1; \
tsm topology set-process -n node${n} -pr vizqlserver -c 2; \
tsm topology set-process -n node${n} -pr cacheserver -c 2; \
tsm topology set-process -n node${n} -pr searchserver -c 1; \
tsm topology set-process -n node${n} -pr dataserver -c 2; \
tsm topology set-process -n node${n} -pr clientfileservice -c 1; \
tsm topology set-process -n node${n} -pr tdsservice -c 1; \
tsm topology set-process -n node${n} -pr collections -c 1; \
tsm topology set-process -n node${n} -pr contentexploration -c 1"

    # TFSID 1406141 - Starting in 2022, indexandsearchserver was introduced, and best practice
    #   is to install it on nodes 1, 2, and 3. (The installer adds it to node1 automatically.)
    if (( $MAJOR_VERSION > 2021 )); then
      ssh_cmd+="; tsm topology set-process -n node${n} -pr indexandsearchserver -c 1"
    fi 
  else
    if [[ $1 == $NODE3_IP ]]; then
      n=3
      ssh_cmd="tsm topology set-process -n node${n} -pr clustercontroller -c 1; \
tsm topology set-process -n node${n} -pr clientfileservice -c 1; \
tsm topology set-process -n node${n} -pr backgrounder -c 4; \
tsm topology set-process -n node${n} -pr filestore -c 1"

      if (( $MAJOR_VERSION > 2021 )); then
        ssh_cmd+="; tsm topology set-process -n node${n} -pr indexandsearchserver -c 1"
      fi
    else
      n=4
      ssh_cmd="tsm topology set-process -n node${n} -pr clustercontroller -c 1; \
tsm topology set-process -n node${n} -pr clientfileservice -c 1; \
tsm topology set-process -n node${n} -pr backgrounder -c 4; \
tsm topology set-process -n node${n} -pr filestore -c 1"
    fi
  fi

  colorprint "Configuring node${n}..."; echo
  run_ssh_command_exit_if_error $NODE1_IP "${ssh_cmd}" "Could not configure node${n} on host ${1}."
}

#-------------------------------------------------------------------
# install_node2
#
# $1 - IP address of node2's host
#
install_node2() {
  display_banner "INSTALLING NODE2 on ${NODE2_IP}"
  install_TS_on_host $NODE2_IP; echo
  send_bootstrap_file_to_host $NODE2_IP; echo
  initialize_node $NODE2_IP; echo
  configure_node $NODE2_IP; echo
  apply_pending_changes
}

#-------------------------------------------------------------------
# install_node3
#
# $1 - IP address of node3's host
#
install_node3() {
  display_banner "INSTALLING NODE3 on ${NODE3_IP}"
  install_TS_on_host $NODE3_IP; echo
  send_bootstrap_file_to_host $NODE3_IP; echo
  initialize_node $NODE3_IP; echo
  configure_node $NODE3_IP; echo
  apply_pending_changes
}

#-------------------------------------------------------------------
# install_node4
#
# $1 - IP address of node4's host
#
install_node4() {
  display_banner "INSTALLING NODE4 on ${NODE4_IP}"
  install_TS_on_host $NODE4_IP; echo
  send_bootstrap_file_to_host $NODE4_IP; echo
  initialize_node $NODE4_IP; echo
  configure_node $NODE4_IP; echo
  apply_pending_changes
}

#-------------------------------------------------------------------
# deploy_coordination_service_ensemble
#
deploy_coordination_service_ensemble() {
  colorprint "Deploying coordination ensemble..."; echo
  run_ssh_command_exit_if_error $NODE1_IP \
"tsm stop; tsm topology deploy-coordination-service -n node1,node2,node3 --ignore-prompt" \
"Failed to deploy coordination ensemble to node1, node2, node3."

  run_ssh_command_exit_if_error $NODE1_IP "tsm start" "Could not start server."
  echo
}

#-------------------------------------------------------------------
# wait_for_filestore_to_sync
#
wait_for_filestore_to_sync() {
  colorprint "Waiting for File Store to synchronize..."; echo
  for i in $(seq 1 180);  do
    echo -ne "."
    sleep 10
    local result=$(ssh -q ec2-user@$NODE1_IP tsm status -v | grep -c -E "File Store [0-9]' is synchronizing")
    if [[ $result == 0 ]]; then
      echo
      echo
      return
    fi
  done

  error_message "Timed out waiting for File Store to synchronize."
  clean_up_sensitive_files
  exit 1
}

#-------------------------------------------------------------------
# remove_redundant_processes_from_node1
#
remove_redundant_processes_from_node1() {
  colorprint "Removing redundant processes from node1..."; echo
  local ssh_cmd="tsm topology filestore decommission -n node1 --ignore-prompt; \
tsm topology set-process -n node1 -pr backgrounder -c 0"
  run_ssh_command_exit_if_error $NODE1_IP "${ssh_cmd}" "Could not remove processes from node1."
  echo

  apply_pending_changes
}

#-------------------------------------------------------------------
# display_status
#
# Displays the status of the finished cluster.
#
display_status() {
  display_banner "FINISHED"

  colorprint "Server status:"; echo
  ssh -t -q ec2-user@$NODE1_IP "tsm status -v | grep -A1 node[1-9]"
  echo
}

#-------------------------------------------------------------------
# display_elapsed_time
#
# Calculates the difference between the provided end and start times
# and displays it in h:m:s format.
# $1 - Start time in seconds
# $2 - End time in seconds
#
display_elapsed_time() {
  local start_sec=$1
  local end_sec=$2

  elapsed_sec=$(( $end_sec - $start_sec ))
  colorprint "Elapsed time: " $CYAN
  if [[ $elapsed_sec -gt 3600 ]]; then
    colorprint "$(( $elapsed_sec / 3600 ))h:" $CYAN
    elapsed_sec=$(( $elapsed_sec % 3600 ))
  fi

  if [[ $elapsed_sec -gt 60 ]]; then
    colorprint "$(( $elapsed_sec / 60 ))m:" $CYAN
    elapsed_sec=$(( $elapsed_sec % 60 ))
  fi
  colorprint "${elapsed_sec}s" $CYAN
  echo
}

#-------------------------------------------------------------------
# generate_config_file
#
# Generates the given file name in the current directory with the
# default values from the EDG.
#
# $1 - config file path
#
generate_config_file() {
  #TODO: Comment out the USE_TSR value once bug 1375232 has been fixed.

  # Values the user must provide
  cat << EOF > $1
# Configuration file for $(basename "$0")

# Entries must be of the form <key>=<value>, with no extra spaces.
# Values in this file will supersede their respective command-line options.

# IPv4 Addresses to the various hosts
# Example: NODE1_IP=44.3.22.111
NODE1_IP=
NODE2_IP=
NODE3_IP=
NODE4_IP=
PGSQL_IP=

# Licensing - specify additional licenses by separating them with commas.
# Single license example:   LICENSE_KEY=1111-2222-3333-4444-5555
# Multiple license example: LICENSE_KEY=AAAA-BBBB-CCCC-DDDD-EEEE,1111-2222-3333-4444-5555
LICENSE_KEY=

# Registration - full path to the registration JSON file.  For more information,
#                search "tsm register" at https://help.tableau.com.
REGISTRATION_FILE=

# Installation information
# IMPORTANT! Edit this URL for your particular installer.
# The minimum supported version for the Enterprise Deployment Guide is 2021.2.2.
# https://downloads.tableau.com/esdalt/2021.2.2/tableau-server-2021-2-2.x86_64.rpm
INSTALLER_URL=
INSTALL_PATH=$DEFAULT_INSTALL_PATH

# File path and name for the Step 1 tar recovery file on PostgreSQL host.
# See Part 4 of the Enterprise Deployment Guide for more information about
# generating a tar recovery file.
# This file is used to restore the state of PostgresSQL prior to installing Tableau server.
# According to the Enterprise Deployment Guide, this value will be /var/lib/pgsql/step1.13.bkp.tar
# or /var/lib/pgsql/step1.12.bkp.tar.
# Comment out the following line if you do not want $(basename "$0") to restore the PostgreSQL
# host to its original state.
PGSQL_TAR_FILE=

# Control color output.  0 = no coloring, 1 = custom coloring (default)
USE_COLOR=1

# Override the Tableau Server installer default setting for the Authorization-to-Run (ATR) service.
# 0 = do not use ATR, 1 = use ATR.
USE_ATR=0

# User name for the initial Tableau Server administrator account.
INITIAL_USERNAME=

EOF

  if [[ ! -f $1 ]]; then
    error_message "Unable to generate configuration file $1."
    exit 1
  fi

  echo "Generated configuration file ${1}."
  colorprint "IMPORTANT: " $RED
  echo "You must edit this file to provide the necessary information \
prior to running $(basename "$0")."
  echo
}

#-------------------------------------------------------------------
# load_config_file
#
# $1 - path of the config file
#
load_config_file() {
  if [[ -z $1 || ! -f $1 ]]; then
    error_message "Invalid configuration file: ${1}."
    exit 1
  fi

  local line
  while read -r line || [[ -n "$line" ]]; do
    # Ignore comments and empty lines
    [[ $line =~ ^#.*  || -z $line ]] && continue

    IFS='=' read -r -a pair <<< "${line}"

    if (( ${#pair[*]} != 2 )); then
      error_message "Invalid configuration file format: ${line}"
      exit 1
    fi

    case "${pair[0]}" in
      NODE1_IP)
        NODE1_IP="${pair[1]}"
        ;;
      NODE2_IP)
        NODE2_IP="${pair[1]}"
        ;;
      NODE3_IP)
        NODE3_IP="${pair[1]}"
        ;;
      NODE4_IP)
        NODE4_IP="${pair[1]}"
        ;;
      PGSQL_IP)
        PGSQL_IP="${pair[1]}"
        ;;
      LICENSE_KEY)
        LICENSE_KEY="${pair[1]}"
        ;;
      REGISTRATION_FILE)
        REGISTRATION_FILE="${pair[1]}"
        ;;
      INITIAL_USERNAME)
        INITIAL_USERNAME="${pair[1]}"
        ;;
      INSTALLER_URL)
        INSTALLER_URL="${pair[1]}"
        ;;
      INSTALL_PATH)
        INSTALL_PATH="${pair[1]}"
        ;;
      PGSQL_TAR_FILE)
        PGSQL_TAR_FILE="${pair[1]}"
        ;;
      PGSQL_PWD_UNSAFE)
        PGSQL_PWD_UNSAFE="${pair[1]}"
        ;;
      INITIAL_USER_PWD_UNSAFE)
        INITIAL_USER_PWD_UNSAFE="${pair[1]}"
        ;;
      USE_COLOR)
        local flag="${pair[1]}"
        if [[ $flag == "1" ]]; then
          USE_COLOR=1
        else
          USE_COLOR=0
        fi
        ;;
      USE_ATR)
        local flag="${pair[1]}"
        if [[ $flag == "1" ]]; then
          USE_ATR=1
        else
          USE_ATR=0
        fi
        ;;
      *)
        error_message "Unknown key ${pair[0]} in configuration file ${1}."
        # Do not delete the config file so the user has a chance to fix the issue.
        exit 1
        ;;
    esac
  done < "${1}"

  # Security: delete the config file now that it's been loaded
  rm -f $1
}

#-------------------------------------------------------------------
# prompt_for_info
#
# Prompts the user to enter info for a variable.
#
# $1 - The name of the variable
# $2 - The prompt to display
# $3 - OPTIONAL - true = password prompt, false = regular prompt
#
prompt_for_info() {
  local is_pwd=${3:-false}
  local input
  while [[ -z $input ]]
  do
    if [[ $is_pwd == true ]]; then
      echo -ne "${2}"
      read -s
      echo
    else
      read -r -p "${2}"
    fi
    input="$REPLY"
  done

  case "$1" in
    NODE1_IP)
      NODE1_IP="${input}"
      ;;
    NODE2_IP)
      NODE2_IP="${input}"
      ;;
    NODE3_IP)
      NODE3_IP="${input}"
      ;;
    NODE4_IP)
      NODE4_IP="${input}"
      ;;
    PGSQL_IP)
      PGSQL_IP="${input}"
      ;;
    LICENSE_KEY)
      LICENSE_KEY="${input}"
      ;;
    REGISTRATION_FILE)
      REGISTRATION_FILE="${input}"
      ;;
    PGSQL_PWD_UNSAFE)
      PGSQL_PWD_UNSAFE="${input}"
      ;;
    INSTALLER_URL)
      INSTALLER_URL="${input}"
      ;;
    INSTALL_PATH)
      INSTALL_PATH="${input}"
      ;;
    INITIAL_USERNAME)
      INITIAL_USERNAME="${input}"
      ;;
    INITIAL_USER_PWD_UNSAFE)
      INITIAL_USER_PWD_UNSAFE="${input}"
      ;;
    *)
      error_message "FATAL ERROR: unknown option in prompt_for_info()."
      clean_up_sensitive_files
      exit 1
      ;;
  esac
}


#-------------------------------------------------------------------
# prompt_for_missing_data
#
# Prompts the user for any missing data needed for installation.
#
prompt_for_missing_data() {
  local ip_prompt="Enter the host IPv4 address for"
  local TS="Tableau Server"

  if [[ -z $NODE1_IP ]]; then prompt_for_info NODE1_IP "${ip_prompt} node1: "; fi
  if [[ -z $NODE2_IP ]]; then prompt_for_info NODE2_IP "${ip_prompt} node2: "; fi
  if [[ -z $NODE3_IP ]]; then prompt_for_info NODE3_IP "${ip_prompt} node3: "; fi
  if [[ -z $NODE4_IP ]]; then prompt_for_info NODE4_IP "${ip_prompt} node4: "; fi
  if [[ -z $PGSQL_IP ]]; then prompt_for_info PGSQL_IP "${ip_prompt} PostgreSQL: "; fi
  if [[ -z $LICENSE_KEY ]]; then prompt_for_info LICENSE_KEY "Enter the license key(s): "; fi
  if [[ -z $REGISTRATION_FILE ]]; then prompt_for_info REGISTRATION_FILE \
"Enter the full path and name of the registration json file: "; fi
  if [[ -z $PGSQL_PWD_UNSAFE ]]; then prompt_for_info PGSQL_PWD_UNSAFE \
"Enter the password for the PostgreSQL connection: " true; fi
  if [[ -z $INSTALLER_URL ]]; then prompt_for_info INSTALLER_URL "Enter the ${TS} installer url: "; fi
  if [[ -z $INITIAL_USERNAME ]]; then prompt_for_info INITIAL_USERNAME \
    "Enter the user name for the initial ${TS} administrator account: "; fi
  if [[ -z $INITIAL_USER_PWD_UNSAFE ]]; then prompt_for_info INITIAL_USER_PWD_UNSAFE \
"Enter the password for the initial ${TS} administrator account: " true; fi
}

#-------------------------------------------------------------------
# parse_cmd_line_args
#
parse_cmd_line_args() {
  OPTIND=1
  while getopts "1:2:3:4:5:a:c:f:g:i:k:p:r:t:u:L:P:-:h" opt; do
    case "$opt" in
      1)
        NODE1_IP="${OPTARG}"
        ;;
      2)
        NODE2_IP="${OPTARG}"
        ;;
      3)
        NODE3_IP="${OPTARG}"
        ;;
      4)
        NODE4_IP="${OPTARG}"
        ;;
      5)
        PGSQL_IP="${OPTARG}"
        ;;
      a)
        if [[ $OPTARG != "1" ]]; then
          USE_ATR=0
        else
          USE_ATR=1
        fi
        ;;
      c)
        if [[ $OPTARG != "1" ]]; then
          USE_COLOR=0
        else
          USE_COLOR=1
        fi
        ;;
      f)
        CONFIG_FILE="${OPTARG}"
        ;;
      g)
        generate_config_file "${OPTARG}"
        exit 0
        ;;
      h)
        usage
        exit 0
        ;;
      i)
        INSTALLER_URL="${OPTARG}"
        ;;
      k)
        LICENSE_KEY="${OPTARG}"
        ;;
      r)
        REGISTRATION_FILE="${OPTARG}"
        ;;
      t)
        PGSQL_TAR_FILE="${OPTARG}"
        ;;
      u)
        INITIAL_USERNAME="${OPTARG}"
        ;;
      L)
        INSTALL_PATH="${OPTARG}"
        ;;
      # Undocumented, unsafe options
      p)
        INITIAL_USER_PWD_UNSAFE="${OPTARG}"
        ;;
      P)
        PGSQL_PWD_UNSAFE="${OPTARG}"
        ;;
      -)
        # Handling for long options
        case "${OPTARG}" in
          node1-ip) NODE1_IP="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          node2-ip) NODE2_IP="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          node3-ip) NODE3_IP="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          node4-ip) NODE4_IP="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          pgsql-ip) PGSQL_IP="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          color) if [[ ${!OPTIND} != "1" ]]; then USE_COLOR=0; else USE_COLOR=1; fi; OPTIND=$(( $OPTIND + 1 ));;
          config-file) CONFIG_FILE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          gen-config-file) generate_config_file "${!OPTIND}"; exit 0;;
          initial-username) INITIAL_USERNAME="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          install-path) INSTALL_PATH="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          installer-url) INSTALLER_URL="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          help) usage; exit 0;;
          license-key) LICENSE_KEY="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          registration-file) REGISTRATION_FILE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          pgsql-tar-file) PGSQL_TAR_FILE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          # Undocumented, unsafe options.  Using these will put plaintext passwords in the bash history.
          pgsql-password-unsafe) PGSQL_PWD_UNSAFE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          initial-user-password-unsafe) INITIAL_USER_PWD_UNSAFE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ));;
          use-atr)
            if [[ ${!OPTIND} != "1" ]]; then USE_ATR=0; else USE_ATR=1; fi;
            OPTIND=$(( $OPTIND + 1 ))
            ;;
          *)
            error_message "Unknown option --${OPTARG}."
            exit 1
            ;;
        esac
        ;;
    esac
  done
  shift "$((OPTIND-1))"

  if [[ -n $CONFIG_FILE ]]; then
    if [[ -f $CONFIG_FILE ]]; then
      load_config_file $CONFIG_FILE
    else
      error_message "Config file ${CONFIG_FILE} does not exist.  Use the -g option to create one."
      exit 1
    fi
  fi
}

#-------------------------------------------------------------------
# verify_connectivity
#
# Verifies that all the hosts are responsive.  Exits upon error.
#
verify_connectivity() {
  local hosts_array=($NODE1_IP $NODE2_IP $NODE3_IP $NODE4_IP $PGSQL_IP)
  local failures=()
  for ip in ${hosts_array[@]}; do
    # Attempt to ssh but use no credentials.  If the host is allowing ssh, then it should
    # return the text "Permission denied".  Otherwise, assume the host is unavailable.
    if ! ssh -o BatchMode=yes -o ConnectTimeout=5 -o PubkeyAuthentication=no -o PasswordAuthentication=no \
-o KbdInteractiveAuthentication=no -o ChallengeResponseAuthentication=no \
${ip} 2>&1 | fgrep -q -i "Permission denied"; then
      failures+=("${ip}")
    fi
  done

  if (( ${#failures[*]} > 0 )); then
    colorprint "ERROR: " $RED
    colorprint "The following hosts are not responding:"
    echo
    for failure in "${failures[@]}"
    do
      echo "  ${failure}"
    done
    colorprint "$(basename "$0") cannot continue.  Verify that the hosts are online \
and accepting SSH traffic before retrying."
    echo
    exit 1
  fi
}

#-------------------------------------------------------------------
# verify_initial_conditions
#
# Performs pre-checks for conditions that would cause the script to fail.
#
verify_initial_conditions() {
  verify_connectivity

  # Registration file must exist
  if [[ ! -f $REGISTRATION_FILE ]]; then
    error_message "Cannot find registration file ${REGISTRATION_FILE}.  $(basename "$0") cannot continue."
    exit 1
  fi
}

#-------------------------------------------------------------------
# parse_installer_version
#
# Determines the major, minor, and maintenance release versions from
# the specified installer
#
parse_installer_version() {
  # Find the XXXX-XX-XX version in the installer URL, which for linux is assumed to end with this format:
  # <blah blah>/tableau-server-M-m-r.x86_64.rpm, where
  # M = major version (year)
  # m = minor version (quarter)
  # r = maintenance release (or "dev" for dev version)

  local version=(`echo ${INSTALLER_URL} | grep -Eo "[0-9]{4}\-[0-9]+\-([0-9]+|dev)" | tail -1 | tr '-' ' '`)
  # M = ${version[0]}
  # m = ${version[1]}
  # r = ${version[2]}

  local err_unrecognized="The specified installer URL contains an unrecognized Tableau Server version."

  if [[ -z $version ]]; then
    error_message "${err_unrecognized}"
    exit 1
  fi

  # Sanity - the following conditions shouldn't happen if regexp is working properly
  if (( ${#version[@]} != 3 )); then
    error_message "${err_unrecognized}"
    exit 1
  fi

  if [[ -z ${version[0]} || -z ${version[1]} || -z ${version[2]} ]]; then
    error_message "${err_unrecognized}"
    exit 1
  fi

  MAJOR_VERSION=${version[0]}
  MINOR_VERSION=${version[1]}
  MAINT_REL_VERSION=${version[2]}

  # Verify the version numbers.
  local min_version_err="Cannot install version ${MAJOR_VERSION}.${MINOR_VERSION}.${MAINT_REL_VERSION}.  \
The minimum supported installer version is 2021.2.2."
  if (( $MAJOR_VERSION < 2021 )); then
    error_message "${min_version_err}"
    exit 1
  elif (( $MAJOR_VERSION == 2021 && $MINOR_VERSION < 2 )); then
    error_message "${min_version_err}"
    exit 1
  elif (( $MAJOR_VERSION == 2021 && $MINOR_VERSION == 2 )); then
    if [[ ${MAINT_REL_VERSION} == "dev" ]]; then
       return
    elif (( $MAINT_REL_VERSION < 2 )); then
      error_message "${min_version_err}"
      exit 1
    fi
  fi
}

main() {
  local NODE1_IP
  local NODE2_IP
  local NODE3_IP
  local NODE4_IP
  local PGSQL_IP
  local LICENSE_KEY

  # Enterprise Deployment Guide (EDG) defaults
  # Minimum supported version for INSTALLER_URL is
  #   https://downloads.tableau.com/esdalt/2021.2.2/tableau-server-2021-2-2.x86_64.rpm
  local INSTALLER_URL

  # A registration file must be provided
  local REGISTRATION_FILE

  readonly DEFAULT_INSTALL_PATH="/app/tableau_server"
  local INSTALL_PATH="${DEFAULT_INSTALL_PATH}"
  
  # If this is empty, then the PGSQL tar file will not be restored.
  # From part 4 of the Enterprise Deployment Guide, the value should be /var/lib/pgsql/step1.12.bkp.tar.
  local PGSQL_TAR_FILE

  local BOOTSTRAP_FILE="boot.$(basename "$0").json"
  local PGSQL_CONFIG_FILE="pgsql.config.$(basename "$0").json"
  local CONFIG_FILE

  local MAJOR_VERSION
  local MINOR_VERSION
  local MAINT_REL_VERSION

  local INITIAL_USERNAME

  # WARNING: hard-coding these poses a security risk!
  local PGSQL_PWD_UNSAFE
  local INITIAL_USER_PWD_UNSAFE

  # Flags
  local USE_COLOR=1

  # TODO: Due to bug 1375232, we need to turn off ATR manually.  In the future, this should
  # be unset, as it's an optional parameter.
  local USE_ATR=0

  local start_sec=$(date +%s)

  parse_cmd_line_args "$@"
  prompt_for_missing_data
  verify_initial_conditions
  parse_installer_version
  restore_pgsql_to_initial_state
  install_initial_node
  install_node2
  install_node3
  deploy_coordination_service_ensemble
  install_node4
  wait_for_filestore_to_sync
  remove_redundant_processes_from_node1
  wait_for_filestore_to_sync
  display_status
  display_elapsed_time $start_sec $(date +%s)
  echo

  # Security: ensure sensitive files are deleted
  clean_up_sensitive_files
}

main "$@"