From ff489a639798c62a1246114aad71e27a7913fa92 Mon Sep 17 00:00:00 2001 From: Reid Vandewiele Date: Thu, 24 Feb 2022 16:39:01 -0800 Subject: [PATCH 1/2] Try to ensure LANG is valid If Ruby starts without a LANG env var, it will assume locale "C". This is a non-UTF-8 locale, and usually not the system default. If commands print UTF-8 characters, Ruby will fail to correctly parse them if it wasn't started with a valid locale. The main scenario we want to account for is when a privilege escalation command such as sudo or powerbroker has elevated a shell, but stripped all environment variables out of it. If that happened, we want to try and set the default system locale, which should be in /etc/default/locale. --- tasks/agent_upgrade.sh | 3 +++ tasks/enable_replica.sh | 3 +++ tasks/pe_install.sh | 7 +++---- tasks/provision_replica.sh | 3 +++ tasks/puppet_runonce.sh | 4 ++++ tasks/wait_until_service_ready.sh | 0 6 files changed, 16 insertions(+), 4 deletions(-) mode change 100644 => 100755 tasks/wait_until_service_ready.sh diff --git a/tasks/agent_upgrade.sh b/tasks/agent_upgrade.sh index 269c2034b..56bd277e1 100755 --- a/tasks/agent_upgrade.sh +++ b/tasks/agent_upgrade.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Try and ensure locale is correctly configured +[ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p') + export USER=$(id -un) export HOME=$(getent passwd "$USER" | cut -d : -f 6) export PATH="/opt/puppetlabs/bin:${PATH}" diff --git a/tasks/enable_replica.sh b/tasks/enable_replica.sh index 4775e6cbe..fcf960f7d 100755 --- a/tasks/enable_replica.sh +++ b/tasks/enable_replica.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Try and ensure locale is correctly configured +[ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p') + USER=$(id -un) HOME=$(getent passwd "$USER" | cut -d : -f 6) diff --git a/tasks/pe_install.sh b/tasks/pe_install.sh index 6288cec70..1c49dcb94 100755 --- a/tasks/pe_install.sh +++ b/tasks/pe_install.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Try and ensure locale is correctly configured +[ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p') + # This stanza configures PuppetDB to quickly fail on start. This is desirable # in situations where PuppetDB WILL fail, such as when PostgreSQL is not yet # configured, and we don't want to let PuppetDB wait five minutes before @@ -20,10 +23,6 @@ pedir=$(tar -tf "$PT_tarball" | head -n 1 | xargs dirname) tar -C "$tgzdir" -xzf "$PT_tarball" -export LANG=en_US.UTF-8 -export LANGUAGE=en_US.UTF-8 -export LC_ALL=en_US.UTF-8 - if [ ! -z "$PT_peconf" ]; then /bin/bash "${tgzdir}/${pedir}/puppet-enterprise-installer" -y -c "$PT_peconf" else diff --git a/tasks/provision_replica.sh b/tasks/provision_replica.sh index c46826e7b..49633ca36 100755 --- a/tasks/provision_replica.sh +++ b/tasks/provision_replica.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Try and ensure locale is correctly configured +[ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p') + export USER=$(id -un) export HOME=$(getent passwd "$USER" | cut -d : -f 6) export PATH="/opt/puppetlabs/bin:${PATH}" diff --git a/tasks/puppet_runonce.sh b/tasks/puppet_runonce.sh index b1b1af034..1accc186e 100755 --- a/tasks/puppet_runonce.sh +++ b/tasks/puppet_runonce.sh @@ -1,5 +1,9 @@ #!/bin/bash +# Try and ensure locale is correctly configured +[ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p') + +# Parse noop parameter [ "$PT_noop" = "true" ] && NOOP_FLAG="--noop" || unset NOOP_FLAG # Wait for up to five minutes for an in-progress Puppet agent run to complete diff --git a/tasks/wait_until_service_ready.sh b/tasks/wait_until_service_ready.sh old mode 100644 new mode 100755 From 5846e95e6f3c1b4dc29490930fe84f52b5e202c5 Mon Sep 17 00:00:00 2001 From: Reid Vandewiele Date: Fri, 25 Feb 2022 09:34:03 -0800 Subject: [PATCH 2/2] Add task_helper to reveal stderr output Bolt tasks are very difficult to debug because they hide stderr output. This commit adds a task helper which should reveal that output. --- files/task_helper.sh | 272 ++++++++++++++++++++++++++++++++++++++++++ tasks/pe_install.json | 3 + tasks/pe_install.sh | 1 + 3 files changed, 276 insertions(+) create mode 100644 files/task_helper.sh diff --git a/files/task_helper.sh b/files/task_helper.sh new file mode 100644 index 000000000..bfd550264 --- /dev/null +++ b/files/task_helper.sh @@ -0,0 +1,272 @@ +#!/bin/bash +#============================================================================= +# Bash Task Helper +# +# This helper allows shell script authors to easily write Bolt tasks which +# return useful output, including success, failure, and key-value return data. +# +# - Set your script shebang line to bash +# - Source this script in the second line of your task +# - For a task parameter "input", you may reference its value using ${input} +# - Use `task-output "key" "value"` to set return data strings +# - Use `task-succeed "message"`, or `task-fail "message"` to end the task +# - The task helper reserves two file descriptors in order to manage and +# process script output into valid task JSON. Consumers MUST NOT use or +# redirect the reserved file descriptors 6 and 7. +# - The task helper sets up a default EXIT trap. If consumers trap EXIT +# themselves, they MUST call `task-exit` at the end of their trap to trigger +# the helper's output finalization routine. +# - When debugging, optionally call `task-verbose-output` before exiting. It +# is recommended only to use this function call when debugging. +# +# Output: +# +# If the script exits with a non-zero exit code, or calls task-fail, all output +# will be returned to Bolt, including any keys set by `task-output`, and the +# message given to `task-fail` (if given). +# +# If the script exits successfully, or if task-succeed is called, no output +# will be returned except output specifically designated by the user via +# `task-output` and/or `task-succeed` function calls. +# +# Examples: +# +# #!/bin/bash +# source "$(dirname $0)/../../bash_task_helper/files/task_helper.sh" +# +# echo "this output will be visible, but not set any key-value data" +# +# VAR=$(date) +# task-output "timestamp" "${VAR}" +# +# task-succeed "demonstration task successful" +# +#============================================================================= + + +# Public: Set status=error, set a message, and exit the task +# +# This function ends the task. The task will return as failed. The function +# accepts an argument to set the task's return message, and an optional exit +# code to use. +# +# $1 - Message. A text string message to return in the task's `message` key. +# $2 - Exit code. A non-zero integer to use as the task's exit code. +# +# Examples +# +# task-fail +# task-fail "task failed because of reasons" +# task-fail "task failed because of reasons" "127" +# +task-fail() { + task-output "status" "error" + _task_exit_string="$1" + task-exit ${2:-1} +} + +# DEPRECATED +fail() { + task-output "_deprecation_warning" "WARN: bash_task_helper fail() is deprecated. Please use task-fail() instead." + task-fail "$@" +} + +# Public: Set status=success, set a message, and exit the task +# +# This function ends the task. The task will return as successful. The function +# accepts an argument to set the task's return message. +# +# $1 - Message. A text string message to return in the task's `message` key. +# +# Examples +# +# task-succeed +# task-succeed "task completed successfully" +# +task-succeed() { + task-output "status" "success" + _task_exit_string="$1" + task-exit 0 +} + +# DEPRECATED +success() { + task-output "_deprecation_warning" "WARN: bash_task_helper: use of success() is deprecated. Please use task-succeed() instead." + task-succeed "$@" +} + +# Public: Set a task output key to a string value +# +# Takes a key argument and a value argument, and ensures that upon task exit +# the key and value will be returned as part of the task output. +# +# $1 - Output key. Should contain only characters that match [A-Za-z0-9-_] +# $2 - Output value. Should be a string. Will be json-escaped. +# +# Examples +# +# task-output "message" "an armadilo crossed the street" +# task-output "maximum" "100" +# +task-output() { + local key="${1}" + local value=$(echo -n "$2" | task-json-escape) + + # Try to find an index for the key + for i in "${!_task_output_keys[@]}"; do + [[ "${_task_output_keys[$i]}" = "${key}" ]] && break + done + + # If there's an index, set its value. Otherwise, add a new key + if [[ "${_task_output_keys[$i]}" = "${key}" ]]; then + _task_output_values[$i]="${value}" + else + _task_output_keys=("${_task_output_keys[@]}" "${key}") + _task_output_values=("${_task_output_values[@]}" "${value}") + fi +} + +# Public: Set the task to always return full output +# +# Tasks normally do not return all output if the task returns successfully. If +# this function is invoked, the task will return all output regardless of exit +# code. +# +# $1 - true or false. Defaults to true. Pass false to turn verbose output off. +# +# Examples +# +# task-verbose-output +# +task-verbose-output() { + _task_verbose_output=${1:-true} +} + +# Public: read text on stdin and output the text json-escaped +# +# A filter command which does its best to json-escape text input. Because the +# function is constrained to rely only on lowest-common-denominator posix +# utilities, it may not be able to fully escape all text on all platforms. +# +# Examples +# +# printf "a string\nwith newlines\n" | task-json-escape +# task-json-escape < file.txt +# +task-json-escape() { + # This is imperfect, and will miss some characters. If we can figure out a + # way to get iconv to catch more character types, we might improve that. + # 1. Replace backslashes with escape sequences + # 2. Replace unicode characters (if possible) with system iconv + # 3. Replace other required characters with escape sequences + # Note that this includes two control-characters specifically + # 4. Escape newlines (1/2): Replace all newlines with literal tabs + # 5. Escape newlines (2/2): Replace all literal tabs with newline escape sequences + # 6. Delete any remaining non-printable lines from the stream + sed -e 's/\\/\\/g' \ + | { iconv -t ASCII --unicode-subst="\u%04x" || cat; } \ + | sed -e 's/"/\\"/' \ + -e 's/\//\\\//g' \ + -e "s/$(printf '\b')/\\\b/" \ + -e "s/$(printf '\f')/\\\f/" \ + -e 's/\r/\\r/g' \ + -e 's/\t/\\t/g' \ + -e "s/$(printf "\x1b")/\\\u001b/g" \ + -e "s/$(printf "\x0f")/\\\u000f/g" \ + | tr '\n' '\t' \ + | sed 's/\t/\\n/g' \ + | tr -cd '\11\12\15\40-\176' +} + +# Public: Print json task return data on task exit +# +# This function is called by a task helper EXIT trap. It will print json task +# return data on task termination. The return data will include all output +# keys set using task-output, and all uncaptured stdout/stderr output produced +# by the script. This function should not be directly invoked, except inside a +# user-created EXIT trap. +# +# $1 - Exit code to terminate script with. Defaults to $?. +# +# Examples +# +# task-exit +# task-exit 1 +# +task-exit() { + # Record the exit code + local exit_code=${1:-$?} + local output + + # Unset the trap + trap - EXIT + + # If appropriate, set an _output value. By default, if the task is + # successful, full script output is suppressed. If the user passed a message + # to task-succeed, that will still be returned as _output. If the task does + # not exit successfully, or if the task is running in verbose mode, then full + # output is returned (including a task-fail user message, if there is one) + if [ "$exit_code" -ne 0 -o "$_task_verbose_output" = 'true' ]; then + # Print the exit string, then set _output to everything that the script has printed + echo -n "$_task_exit_string" + task-output '_output' "$(cat "${_output_tmpfile}")" + elif [ ! -z "$_task_exit_string" ]; then + # Set _output to just the exit string + task-output '_output' "${_task_exit_string}" + fi + + # Reset outputs + exec 1>&6 + exec 2>&7 + + # Print JSON to stdout + printf '{\n' + for i in "${!_task_output_keys[@]}"; do + # Print each key-value pair + printf ' "%s": "%s"' "${_task_output_keys[$i]}" "${_task_output_values[$i]}" + # Print a comma unless it's the last key-value + [ ! "$(($i + 1))" -eq "${#_task_output_keys[@]}" ] && printf ',' + # Print a newline + printf '\n' + done + printf '}\n' + + # Remove the output tempfile + rm "$_output_tmpfile" + + # Resume an orderly exit + exit "$exit_code" +} + +# Test for colors. If unavailable, unset variables are ok +if tput colors &>/dev/null; then + green="$(tput setaf 2)" + red="$(tput setaf 1)" + reset="$(tput sgr0)" +fi + +# Use indirection to munge PT_ environment variables +# e.g. "$PT_version" becomes "$version" +for v in ${!PT_*}; do + declare "${v#*PT_}"="${!v}" +done + +# Set up variables to record task outputs +_task_output_keys=() +_task_output_values=() +_task_exit_string='' +_task_verbose_output=false + +# Redirect all output (stdin, stderr) to a tempfile, and trap EXIT. Upon exit, +# print a Bolt task return JSON string, with the full contents of the tempfile +# in the "_output" key. +# +# Note: file descriptors 6 and 7 are used to save original stdout/stderr. These +# were chosen as the file descriptors least likely to be used by shell +# script task authors. Client scripts MUST NOT use these descriptors. +_output_tmpfile="$(mktemp)" +trap task-exit EXIT +exec 6>&1 \ + 7>&2 \ + 1>> "$_output_tmpfile" \ + 2>&1 diff --git a/tasks/pe_install.json b/tasks/pe_install.json index 454200acf..8af56122a 100644 --- a/tasks/pe_install.json +++ b/tasks/pe_install.json @@ -19,6 +19,9 @@ } }, "input_method": "environment", + "files": [ + "peadm/files/task_helper.sh" + ], "implementations": [ {"name": "pe_install.sh"} ] diff --git a/tasks/pe_install.sh b/tasks/pe_install.sh index 1c49dcb94..8e8736329 100755 --- a/tasks/pe_install.sh +++ b/tasks/pe_install.sh @@ -1,4 +1,5 @@ #!/bin/bash +source "$(dirname $0)/../../peadm/files/task_helper.sh" # Try and ensure locale is correctly configured [ -z "${LANG}" ] && export LANG=$(localectl status | sed -n 's/.* LANG=\(.*\)/\1/p')