#!/bin/bash
# -*- mode: shell-script; indent-tabs-mode: t; sh-basic-offset: 8; sh-indentation: 8; sh-indent-for-case-alt: + -*-

script_path="$(dirname ${0})"
script_name="$(basename ${0})"
pbench_bin="$(realpath -e ${script_path}/..)"

# source the base script
. "${pbench_bin}"/base

# The sole purpose of this script is to ensure a tool of your choosing is used
# during the execution of a benchmark on the optional list of remote hostnames
# specified. For a list of performance tools, use the --help option or look at
# the ${pbench_bin}/tool-scripts directory.

#+
# "On Disk" format for a tool group:
#
# The "V1" tools on-disk repository is as follows:
#   * The directory resides at "${pbench_run}/tools-v1-${group}"
#     * note the prefix is now "tools-v1-" before the tools group name
#   * The next level of the hierarchy are directories named for each remote
#     host name tools are register for
#     * If the user registers the local host, we always use the full hostname,
#       ${_pbench_full_hostname}
#     * All other names are considered "remote" host names
#   * Under each host name directory are tool and an optional "__label__" file
#     * Each tool file is named after the tool for which each line of the file
#       contains a tool option
#       * If a file symlink exists named "<tool>.__noinstall__", then auto-
#         installation of the tool has been turned off
#     * "labels" for a host name are recorded in the "__label__" file
#   * Triggers for a given tool group live in the "__trigger__" file in the
#     top-level directory of the tool group
#
# Examples:
#   * A single local host, ${_pbench_full_hostname} == "localhost", with four
#     tools and a trigger:
#
#     ${pbench_run}/
#       tools-v1-default/
#         __trigger__
#         localhost/
#           sar
#           pidstat
#           vmstat
#           iostat
#
#   * Four remote hosts with two tools on all hosts, 1 additional tool on one
#     host, with auto-installation of the tool turned off:
#
#     ${pbench_run}/
#       tools-v1-my2tools/
#         one.example.com/
#           mpstat
#           sar
#         two.example.com/
#           mpstat
#           sar
#         three.example.com/
#           mpstat
#           sar
#         four.example.com/
#           mpstat
#           sar
#           perf
#           perf.__noinstall__@
#
#   * Three hosts, one local (${_pbench_full_hostname} == "localhost"), with
#     two tools on all hosts, 1 additional tool on the local host, labels on
#     all hosts:
#
#     ${pbench_run}/
#       tools-v1-my2tools/
#         localhost/
#           __label__
#           mpstat
#           iostat
#           sar
#         one.example.com/
#           __label__
#           mpstat
#           iostat
#         two.example.com/
#           __label__
#           mpstat
#           iostat
#-

# Defaults
name=""
remotes_arg=""
remotes_def="localhost"
labels_arg=""
group="default"
tool_type="transient"
options=""
do_install=1
_do_test_args=0

function usage() {
	if [[ ${_do_test_args} -eq 1 ]]; then
		# Since we are working on behalf of pbench-register-tool-set,
		# skip printing the usage message here -- it will issue its own
		# usage message on failure.
		return 0
	fi
	# Usage is formatted for a 80 column width screen.
	printf -- "usage:\n"
	printf -- "${script_name} --name=<tool-name> [--group=<group-name>]"
	printf -- " [--no-install] [--persistent] [--transient]"
	printf -- " [--remotes=<remote-host>[,<remote-host>]] [--labels=<label>[,<label>]]"
	printf -- " -- [all tool specific options here]\n"
	printf -- "${script_name} --name=<tool-name> [--group=<group-name>]"
	printf -- " [--no-install] [--persistent] [--transient] [--remotes=@<remotes-file>]"
	printf -- " -- [all tool specific options here]\n\n"
	#             1         2         3         4         5         6         7         8
	# (tab)    1 901234567890123456789012345678901234567890123456789012345678901234567890
	printf -- "\tWhere the list of labels must match the list of remotes, one label per\n"
	printf -- "\thost.\n\n"
	printf -- "\tOne can specify as the argument to the \"--remotes\" option a single\n"
	printf -- "\tremote host, a list of remote hosts (comma-separated, no spaces) or an\n"
	printf -- "\t\"at\" sign (\"@\") followed by a filename.  In this last case, the file\n"
	printf -- "\tshould contain a list of hosts and their (optional) labels.  Each line\n"
	printf -- "\tof the file should contain a host name, optionally followed by a label\n"
	printf -- "\tseparated by a comma (\",\"); empty lines are ignored, and comments are\n"
	printf -- "\tdenoted by a leading hash, or pound (\"#\"), character.\n"
	printf -- "\n\t--persistent or --transient can be used to specify tool run type.\n"
	printf -- "\nAvailable tools:\n"
	local tool=""
	python3 -c "import sys, json; meta = json.load(open(sys.argv[1])); print('  Transient:', *[f'\t{tool}' for tool in meta['transient'].keys()], '  Persistent:', *[f'\t{tool}' for tool in meta['persistent'].keys()], sep='\n')" ${pbench_bin}/tool-scripts/meta.json
	#                     1         2         3         4         5         6         7         8
	# (no tab)   12345678901234567890123456789012345678901234567890123456789012345678901234567890
	printf -- "\nFor a list of tool specific options, run:\n"
	printf -- "\t${pbench_bin}/tool-scripts/<tool-name> --help\n\n"

}

# Process options and arguments
opts=$(getopt -q -o hn:g:r:l: --longoptions "help,name:,group:,no-install,remote:,remotes:,label:,labels:,test-args,persistent,transient" -n "getopt.sh" -- "${@}")
if [[ ${?} -ne 0 ]]; then
	printf -- "\n${*}\n" >&2
	printf -- "${script_name}: you specified an invalid option\n\n" >&2
	usage >&2
	exit 1
fi

# Process only the options and arguments before the "--", which are intended
# for pbench-register-tool itself; all agruments after the "--" are for the
# tool specified by the "--name" option.
eval set -- "${opts}"
while true; do
	case "${1}" in
	-n|--name)
		shift
		if [[ -n "${1}" ]]; then
			name="${1}"
			shift
		fi
		;;
	-r|--remote|--remotes)
		shift
		if [[ -n "${1}" ]]; then
			remotes_arg="${1}"
			shift
		fi
		;;
	-l|--label|--labels)
		shift
		if [[ -n "${1}" ]]; then
			labels_arg="${1}"
			shift
		fi
		;;
	-g|--group)
		shift
		if [[ -n "${1}" ]]; then
			group="${1}"
			shift
		fi
		;;
	--no-install)
		shift
		do_install=0
		;;
	--test-args)
		shift
		_do_test_args=1
		;;
	--persistent)
		shift
		display_name=${name}
		name=$(${pbench_bin}/util-scripts/get-internal-tool ${pbench_bin}/tool-scripts ${name} persistent)
		if [[ -z "${name}" ]]; then
			name="internal-tool-not-found"
		fi
		tool_type="persistent"
		;;
	--transient)
		shift
		display_name=${name}
		name=$(${pbench_bin}/util-scripts/get-internal-tool ${pbench_bin}/tool-scripts ${name} transient)
		if [[ -z "${name}" ]]; then
			name="internal-tool-not-found"
		fi
		tool_type="transient"
		;;
	-h|--help)
		usage
		exit 0
		;;
	--)
		shift
		break
		;;
	esac
done

# Normally --name is required but, when called with --test-args, we allow an
# exception: that allows pbench-register-tool-set to call us without having to
# specify a (meaningless in that case) --name.
if [[ -z "${name}" && "${_do_test_args}" != 1 ]]; then
	error_log "Missing required parameter --name=<tool-name>"
	usage >&2
	exit 1
fi

if [[ ! -x "${pbench_bin}/tool-scripts/${name}" ]]; then
	if [[ ${name} == "internal-tool-not-found" ]]; then
		error_log "Could not find ${display_name} with type ${tool_type} in ${pbench_bin}/tool-scripts/: has the right type been specified?"
	else
		error_log "Could not find ${name} in ${pbench_bin}/tool-scripts/: has this tool been integrated into the pbench-agent?"
	fi
	usage >&2
	exit 1
fi

if [[ ${remotes_arg::1} == "@" ]]; then
	# We are dealing with a file containing the list of remotes.
	remotes_file=${remotes_arg:1}

	if [[ -n ${labels_arg} ]]; then
		error_log "--labels=${labels_arg} not allowed with remotes file (${remotes_arg})"
		usage >&2
		exit 1
	fi

	if [[ ! -r ${remotes_file} ]]; then
		error_log "--remotes=@${remotes_file} specifies a file that does not exist or is not readable"
		usage >&2
		exit 1
	fi

	if [[ ! -s ${remotes_file} ]]; then
		error_log "--remotes=@${remotes_file} specifies an empty file"
		usage >&2
		exit 1
	fi

	declare -a remotes_A
	declare -A labels_A
	idx=0
	lineno=0
	while read line; do
		(( lineno++ ))
		# a comment starts with '#' (optionally preceded by spaces or tabs) and ends at the end of the line
		if [[ ${line} == *([[:blank:]])#* ]]; then
			# Ignore comments
			continue
		fi
		IFS=',' read -ra parts <<< "${line}"
		# we need arithmetic evaluation of the conditionals
		if (( ${#parts[@]} == 0 )); then
			# Ignore empty lines
			continue
		elif (( ${#parts[@]} <= 2 )); then
			# Assume the first part is the remote host name, and
			# the second part (if present) is the label for that host name.
			remotes_A[${idx}]=${parts[0]}
			labels_A[${parts[0]}]=${parts[1]}
		else
			error_log "--remotes=@${remotes_file} specifies a file with an invalid format, expected \"<hostname>[,<label>]\" at line #${lineno}"
			usage >&2
			exit 1
		fi
		(( idx++ ))
	done < ${remotes_file}
else
	# First create the array of remotes and their labels, "remotes_A"
	# "labels_A", from the "remotes_arg" and "labels_arg".
	IFS=',' read -ra remotes_A <<< "${remotes_arg:-${remotes_def}}"
	if [[ -z "${remotes_A}" ]]; then
		error_log "INTERNAL: missing -r|--remote|--remotes=<remote-host>[,<remote-host>] argument for some unknown reason (should not happen)"
		usage >&2
		exit 1
	fi

	declare -A labels_A
	if [[ -n ${labels_arg} ]]; then
		IFS=',' read -ra labels_A0 <<< "${labels_arg}"
		if [[ -n "${labels_A0}" ]]; then
			if [[ ${#remotes_A[@]} -ne ${#labels_A0[@]} ]]; then
				if [[ -z "${remotes_arg}" ]]; then
					remotes="\"${_pbench_full_hostname}\" (default)"
				else
					remotes="\"${remotes_arg}\""
				fi
				error_log "The number of labels given, \"${labels_arg}\", does not match the number of remotes given, ${remotes}"
				usage >&2
				exit 1
			fi
			# Now create an array for labels, where the index is
			# by given hostname from the remotes.
			for (( idx=0; idx<${#labels_A0[@]}; idx++ )); do
				labels_A[${remotes_A[${idx}]}]=${labels_A0[${idx}]}
			done
		fi
	fi
fi

invalids=0
for (( i=0; ${i} < ${#remotes_A[@]}; i++ )); do
	remote="${remotes_A[${i}]}"
	validate-hostname "${remote}"
	if [[ ${?} -ne 0 ]]; then
		error_log "Invalid remote specified: '${remote}'"
		(( invalids++ ))
	fi
done
if [[ ${invalids} -gt 0 ]]; then
	exit 1
fi

if [[ ${_do_test_args} -ne 0 ]]; then
	# Used by pbench-register-tool-set to avoid duplicating logic, we
	# have been asked to exit early successfully if all argument
	# processing has been successful so far.
	exit 0
fi

# The remainder of ${@} is now tool-specific options. The best way to not
# mess up arguments seems to be using an array.
idx=0
declare -a tool_opts
while [[ -n "${1}" ]]; do
	tool_opts[${idx}]="${1}"
	let idx=${idx}+1
	shift
done
tool_opt_array_cmd=""
for (( i=0; ${i} < ${#tool_opts[@]}; i++ )); do
	tool_opt_array_cmd="${tool_opt_array_cmd} tool_opts[${i}]=\"${tool_opts[${i}]}\";"
done
debug_log "tool_opts: \"${tool_opts[@]}\""

# At this point all argument processing is complete.

# Determine what our local hostname(s) and IP(s) are.
# FIXME: this does not check for ipv6
local_ips=$(ip a | grep "inet " | awk '{print $2}' | awk -F/ '{print $1}')
local_interfaces="${local_ips} ${_pbench_hostname} ${_pbench_full_hostname} localhost"

# If for some reason the user specified a remote host, and that host is a
# local interface, we work to make sure we only register one local host.
idx=0
local_hostname=""
local_label=""
declare -a remotes
for (( i=0; ${i} < ${#remotes_A[@]}; i++ )); do
	remote="${remotes_A[${i}]}"
	for local_interface in ${local_interfaces}; do
		if [[ "${remote}" == "${local_interface}" ]]; then
			if [[ -z "${local_hostname}" ]]; then
				if [[ -n "${remotes_arg}" ]]; then
					debug_log "The remote host you have provided, ${remote}, matches a local interface, so we will register this tool locally only"
				fi
				local_hostname="${_pbench_full_hostname}"
				local_label="${labels_A[${remote}]}"
			else
				if [[ -n "${remotes_arg}" ]]; then
					debug_log "The remote host you have provided, ${remote}, matches a local interface, and has already been registered locally"
				fi
			fi
			remote=""
			break
		fi
	done
	if [[ -n "${remote}" ]]; then
		remotes[${idx}]=${remote}
		let idx=${idx}+1
	fi
done

# At this point we are ready to record the tool for the list of hosts. Ensure
# the target tools group directory exists.
tg_dir="$(gen_tools_group_dir ${group})"
if [[ ${?} -ne 0 || -z "${tg_dir}" ]]; then
	error_log "Bad or missing tools group, \"${tg_dir}\""
	exit 1
fi
if [[ ! -d "${tg_dir}" ]]; then
	mkdir "${tg_dir}"
	if [[ ${?} -ne 0 ]]; then
		error_log "Unable to create the necessary tools group directory, \"${tg_dir}\""
		exit 1
	fi
fi

function record_tool() {
	local remote_arg=${1}
	local label_arg=${2}

	if [[ ! -d ${tg_dir}/${remote_arg} ]]; then
		mkdir "${tg_dir}/${remote_arg}"
		if [[ ${?} -ne 0 ]]; then
			error_log "Unable to create the necessary tools group remote directory, \"${tg_dir}/${remote_arg}\""
			return 1
		fi
	fi
	local tg_r_dir="${tg_dir}/${remote_arg}"

	# Record the tool and its options since its installation was
	# successful.
	local this_tool_file="${tg_r_dir}/${name}"
	/bin/rm -f "${this_tool_file}"
	touch "${this_tool_file}"
	local i=0
	# tool options are 1 per line
	for (( i=0; ${i} < ${#tool_opts[@]}; i++ )); do
		echo "${tool_opts[${i}]}" >> "${this_tool_file}"
	done

	if [[ ${do_install} == 0 ]]; then
		if [[ ! -e ${this_tool_file}.__noinstall__ ]]; then
			/bin/ln -s ${this_tool_file} ${this_tool_file}.__noinstall__
			if [[ ${?} -ne 0 ]]; then
				error_log "Unable to create the necessary tools group tool \"no install\" symlink, ${this_tool_file}.__noinstall__"
				return 1
			fi
		fi
	else
		if [[ -e ${this_tool_file}.__noinstall__ ]]; then
			/bin/rm ${this_tool_file}.__noinstall__
			if [[ ${?} -ne 0 ]]; then
				error_log "Unable to remove the previous tools group tool \"no install\" symlink, ${this_tool_file}.__noinstall__"
				return 1
			fi
		fi
	fi

	# If we have a label, update the label in the tool directory
	if [[ "${label_arg}" != "" ]]; then
		local this_label_file
		this_label_file="${tg_r_dir}/__label__"
		if [[ -e "${this_label_file}" ]]; then
			/bin/rm "${this_label_file}"
			if [[ ${?} -ne 0 ]]; then
				error_log "Unable to remove the previous tools group host label file, ${this_label_file}"
				return 1
			fi
		fi
		echo "${label_arg}" > "${this_label_file}"
		_label_msg=", with label \"${label_arg}\","
	else
		_label_msg=""
	fi
	if [[ -z "${display_name}" ]]; then
		display_name=${name}
	fi
	local _msg="\"${display_name}\" tool is now registered for host \"${remote_arg}\"${_label_msg} in group \"${group}\""
	log "${_msg}"
	printf "${_msg}\n"
	return 0
}

rc=0
if [[ -n "${local_hostname}" ]]; then
	record_tool "${local_hostname}" "${local_label}"
	rc=${?}
fi

# Make sure we capture any local error codes to report failure later of the
# overall script.
exit_rc=${rc}

for (( i=0; ${i} < ${#remotes[@]}; i++ )); do
	remote="${remotes[${i}]}"
	label=${labels_A[${remote}]}
	record_tool "${remote}" "${label}"
	rc=${?}
	let exit_rc=${exit_rc}+${rc}
done

exit ${exit_rc}
