#!/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 < IPv4 address for the node1 host. -2,--node2-ip
IPv4 address for the node2 host. -3,--node3-ip
IPv4 address for the node3 host. -4,--node4-ip
IPv4 address for the node4 host. -5,--pgsql-ip
IPv4 address for the PostgreSQL host. -i,--installer-url URL of the Tableau Server installer rpm file. -k,--license-key [,,...] Product key(s) to activate Server license(s), separated by commas. -r,--registration-file Full path to the registration JSON file. For more information, search "tsm register" at https://help.tableau.com. -u,--initial-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 Full path to a configuration file. Values in the configuration file supersede their corresponding command- line options. -g,--gen-config-file 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 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 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 =, 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: # /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 "$@"