# SPDX-License-Identifier: MIT
#
# chreipl-fcp-mpath: use multipath information to change FCP IPL target
# (C) Copyright IBM Corp. 2021
#
# Uses the following system-utilities (and shell-builtins):
#   GNU coreutils:
#     - mktemp
#     - readlink
#     - sync
#   util-linux:
#     - flock
#     - logger

# Makes use of udev event environment variables:
#	SEQNUM

# (1) expand failed globs to an empty string
# (2) extended pattern matching to strip leading/trailing whitespaces
shopt -s nullglob extglob
# (1) don't overwrite existing files using redirects (e.g.: `>`)
set -o noclobber

# make sure any state files created are only writeable by the owning user
umask 027

# create log if DEBUG is enabled (with Make: D=1)
#
# Each script importing this library and expecting a debug log to be created
# must declare a *trace tag* in a variable `debug_trace_tag`. This is used as
# identifier in the log file name. The format is:
#
#     [[:digit:]][[:digit:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]
#     \                    /\                                          /
#      --------\  /--------  -------------------\  /-------------------
#               \/                               \/
#      relative position of  some unique abbreviation for the script
#      execution in the      name, excluding any common prefix
#      udev rules
if '@DEBUG@' && [ -v debug_trace_tag ] && tlg="$(
	mktemp -p '@debugoutdir@'					\
		"chreiplzfcpmp-${debug_trace_tag}-${SEQNUM:-0}.XXXXXXXXXX" \
		2>/dev/null)"
then
	readonly tlg
	exec >|"${tlg}" 2>&1
	set -x
	set
else
	unset tlg
fi

declare -gr ID_FILE='@chreiplzfcpmp-id-file@'
declare -gr FW_LOCK_FILE='@chreiplzfcpmp-fwlock-file@'

declare -gA TRAP_EXIT_FN=()
declare -gf trap_exit 1>/dev/null
function trap_exit() {
	local fn

	for fn in "${TRAP_EXIT_FN[@]}"; do
		"${fn}"
	done

	trap - EXIT
}
trap trap_exit EXIT

# Output variables:
#  id_file_unlock_exclusive_create() - call to unlock when finished with
#                                      critical section
#
# XXX: `id_file_lock_*` can't be taken recursively
function id_file_lock_exclusive_create() {
	declare -g ID_FILE_LOCK=""

	# prevent concurrent file creation
	#
	# First, open the file defined in ${ID_FILE} for writing; this will
	# succeed and create the file only if it doesn't exist already. If the
	# file already exist, the first open attempt will fail and we fall
	# back to opening it only for reading; this will always succeed if the
	# file already exists (the reason why the first attempty failed). In
	# both cases store the corresponding file descriptor in
	# ${ID_FILE_LOCK}.
	#
	# XXX: This should be race free.
	#      open() with O_EXCL... is atomic (we set `noclobber` as shell
	#      option); at least as long as we talk about a local FS.
	if ! { exec {ID_FILE_LOCK}>"${ID_FILE}"; } 2>/dev/null; then
		{ exec {ID_FILE_LOCK}<"${ID_FILE}"; } 2>/dev/null	\
								|| return 1
	fi

	declare -gf id_file_unlock_exclusive_create 1>/dev/null
	function id_file_unlock_exclusive_create() {
		if [ -v ID_FILE_LOCK ]; then
			sync "${ID_FILE}" 2>/dev/null
			# release file and implicitly the lock, if taken
			exec {ID_FILE_LOCK}>&-
			unset ID_FILE_LOCK
		fi

		unset "TRAP_EXIT_FN[id_file_unlock_exclusive_create]"
	}
	TRAP_EXIT_FN+=(
		[id_file_unlock_exclusive_create]=id_file_unlock_exclusive_create
	)

	flock --exclusive --timeout 5 "${ID_FILE_LOCK}"		|| return 2

	return 0
}

# Output variables:
#  id_file_unlock_exclusive_no_create() - call to unlock when finished with
#                                         critical section
#
# XXX: `id_file_lock_*` can't be taken recursively
function id_file_lock_exclusive_no_create() {
	declare -g ID_FILE_LOCK=""

	# Open the file defined in ${ID_FILE} for reading, and store the
	# corresponding file descriptor in ${ID_FILE_LOCK}.
	#
	# XXX: return code is used in `chreipl-fcp-mpath-try-change-ipl-path`
	{ exec {ID_FILE_LOCK}<"${ID_FILE}"; } 2>/dev/null	|| return 1

	declare -gf id_file_unlock_exclusive_no_create 1>/dev/null
	function id_file_unlock_exclusive_no_create() {
		if [ -v ID_FILE_LOCK ]; then
			sync "${ID_FILE}" 2>/dev/null
			# release file and implicitly the lock, if taken
			exec {ID_FILE_LOCK}<&-
			unset ID_FILE_LOCK
		fi

		unset "TRAP_EXIT_FN[id_file_unlock_exclusive_no_create]"
	}
	TRAP_EXIT_FN+=(
		[id_file_unlock_exclusive_no_create]=id_file_unlock_exclusive_no_create
	)

	flock --exclusive --timeout 5 "${ID_FILE_LOCK}"		|| return 2

	return 0
}

# Output variables:
#  id_file_unlock_shared_no_create() - call to unlock when finished with
#                                      critical section
#
# XXX: `id_file_lock_*` can't be taken recursively
function id_file_lock_shared_no_create() {
	declare -g ID_FILE_LOCK=""

	# Open the file defined in ${ID_FILE} for reading, and store the
	# corresponding file descriptor in ${ID_FILE_LOCK}.
	{ exec {ID_FILE_LOCK}<"${ID_FILE}"; } 2>/dev/null	|| return 1

	declare -gf id_file_unlock_shared_no_create 1>/dev/null
	function id_file_unlock_shared_no_create() {
		if [ -v ID_FILE_LOCK ]; then
			# release file and implicitly the lock, if taken
			exec {ID_FILE_LOCK}<&-
			unset ID_FILE_LOCK
		fi

		unset "TRAP_EXIT_FN[id_file_unlock_shared_no_create]"
	}
	TRAP_EXIT_FN+=(
		[id_file_unlock_shared_no_create]=id_file_unlock_shared_no_create
	)

	flock --shared --timeout 5 "${ID_FILE_LOCK}"		|| return 2

	return 0
}

# Output variables:
#  firmware_unlock_exclusive() - call to unlock when finished with critical section
#
# XXX: `firmware_lock_*` can't be taken recursively
function firmware_lock_exclusive() {
	declare -g FIRMWARE_LOCK=""

	# Open the file defined in ${FW_LOCK_FILE} for reading, and store the
	# corresponding file descriptor in ${FIRMWARE_LOCK} (it doesn't matter
	# whether this is a normal file or directory). This file descriptor
	# will only be used for locking - not for actual I/O.
	{ exec {FIRMWARE_LOCK}<"${FW_LOCK_FILE}"; } 2>/dev/null	|| return 1

	declare -gf firmware_unlock_exclusive 1>/dev/null
	function firmware_unlock_exclusive() {
		if [ -v FIRMWARE_LOCK ]; then
			# release file and implicitly the lock, if taken
			exec {FIRMWARE_LOCK}<&-
			unset FIRMWARE_LOCK
		fi

		unset "TRAP_EXIT_FN[firmware_unlock_exclusive]"
	}
	TRAP_EXIT_FN+=([firmware_unlock_exclusive]=firmware_unlock_exclusive)

	flock --exclusive --timeout 5 "${FIRMWARE_LOCK}"	|| return 2

	return 0
}

# Output variables:
#  firmware_unlock_shared() - call to unlock when finished with critical section
#
# XXX: `firmware_lock_*` can't be taken recursively
function firmware_lock_shared() {
	declare -g FIRMWARE_LOCK=""

	# Open the file defined in ${FW_LOCK_FILE} for reading, and store the
	# corresponding file descriptor in ${FIRMWARE_LOCK} (it doesn't matter
	# whether this is a normal file or directory). This file descriptor
	# will only be used for locking - not for actual I/O.
	{ exec {FIRMWARE_LOCK}<"${FW_LOCK_FILE}"; } 2>/dev/null	|| return 1

	declare -gf firmware_unlock_shared 1>/dev/null
	function firmware_unlock_shared() {
		if [ -v FIRMWARE_LOCK ]; then
			# release file and implicitly the lock, if taken
			exec {FIRMWARE_LOCK}<&-
			unset FIRMWARE_LOCK
		fi

		unset "TRAP_EXIT_FN[firmware_unlock_shared]"
	}
	TRAP_EXIT_FN+=([firmware_unlock_shared]=firmware_unlock_shared)

	flock --shared --timeout 5 "${FIRMWARE_LOCK}"		|| return 2

	return 0
}

# Output variables:
#  IPL_TYPE
#  IPL_BUSID
#  IPL_WWPN
#  IPL_LUN
function firmware_get_ipl_information() {
	declare -g IPL_TYPE="" IPL_BUSID="" IPL_WWPN="" IPL_LUN=""

	# Take lock so we don't see any intermediate state from other helpers
	# running in parallel
	firmware_lock_shared					|| return 5

	{ read -r IPL_TYPE _ < /sys/firmware/reipl/reipl_type; } 2>/dev/null \
								|| return 1
	{ read -r IPL_BUSID _ < /sys/firmware/reipl/fcp/device; } 2>/dev/null \
								|| return 2
	{ read -r IPL_WWPN _ < /sys/firmware/reipl/fcp/wwpn; } 2>/dev/null \
								|| return 3
	{ read -r IPL_LUN _ < /sys/firmware/reipl/fcp/lun; } 2>/dev/null \
								|| return 4

	firmware_unlock_shared

	# show read values in debug log if enabled
	if '@DEBUG@'; then
		declare -p IPL_TYPE IPL_BUSID IPL_WWPN IPL_LUN 1>&2
	fi

	return 0
}

# Input:
#  1: absolute canonical path to the scsi device in sysfs, e.g.:
#     /sys/devices/css0/0.0.0014/0.0.1700/host1/rport-1:0-0/target1:0:0/1:0:0:1075789848
# Output variables:
#  SDEV_LUN
function sdev_get_lun() {
	local sdev="${1}" sdev_lun_str
	# bash uses `intmax_t` as width for integer variables, and glibc
	# defines this either as `long int` on 64 bit systems, or
	# `long long int` on other.
	local -i sdev_lun=0 fcp_lun=0

	sdev_lun_str="${sdev##*:}"
	# e.g.: 1075789848
	[[ "${sdev_lun_str}" == +([[:digit:]]) ]]		|| return 1
	# "cast" to integer
	sdev_lun="${sdev_lun_str}"

	# convert the Linux integer LUN format to the hexadecimal 64 bit T10
	# LUN representation format used by many s390x interfaces
	(( fcp_lun = (((sdev_lun >> 0) & 0xffff) << 48)
			| (((sdev_lun >> 16) & 0xffff) << 32)
			| (((sdev_lun >> 32) & 0xffff) << 16)
			| (((sdev_lun >> 48) & 0xffff) << 0) ))
	# the '0x' prefix is part of the length
	printf -v SDEV_LUN "%#018llx" "${fcp_lun}"

	# show read values in debug log if enabled
	if '@DEBUG@'; then
		declare -p SDEV_LUN 1>&2
	fi

	return 0
}

# Input:
#  1: path to the scsi device in sysfs, e.g.:
#     /sys/devices/css0/0.0.0014/0.0.1700/host1/rport-1:0-0/target1:0:0/1:0:0:1075789848
#     , or a symlink pointing to the scsi device, e.g.:
#     /sys/class/block/sds/device
# Output variables:
#  SDEV_BUSID
#  SDEV_WWPN
#  SDEV_LUN
function sdev_get_fcp_addressing() {
	local sdev="${1}" fcp_lun rport rport_wwpn zfcp_dev

	declare -g SDEV_BUSID="" SDEV_WWPN="" SDEV_LUN=""

	sdev="$(readlink -se "${sdev}")"			|| return 1

	# get the LUN for this SDEV
	#
	# sets ${SDEV_LUN}
	sdev_get_lun "${sdev}"					|| return 2

	# get the WWPN of the remote port this SDEV is attached to
	printf -v rport "%s" "${sdev}"/../../fc_remote_ports/rport-*:*-*
	# e.g.: /sys/devices/css0/0.0.0016/0.0.1740/host0/rport-0:0-1/fc_remote_ports/rport-0:0-1
	[ "${rport}" != "" ]					|| return 3

	# XXX: This works even if the rport is currently in a bad
	#      state, so e.g. when it has just gone down because of a
	#      cable pull.
	{ read -r rport_wwpn _ < "${rport}"/port_name; } 2>/dev/null	\
								|| return 4

	# The Linux kernel doesn't guarantee the same format as in
	# /sys/firmware/..., so make sure it is the one we expect.
	[[ "${rport_wwpn}" =~ ^0x[[:xdigit:]]{1,16}$ ]]		|| return 5
	# the '0x' prefix is part of the length
	printf -v rport_wwpn "%#018llx" "${rport_wwpn}"

	# get the Device Bus-ID of the device via which this SDEV is attached
	zfcp_dev="$(readlink -se "${sdev}"/../../../..)"	|| return 6
	# e.g.: /sys/devices/css0/0.0.0016/0.0.1740
	zfcp_dev="${zfcp_dev##*/}"

	# shellcheck disable=2034
	SDEV_BUSID="${zfcp_dev}"
	# shellcheck disable=2034
	SDEV_WWPN="${rport_wwpn}"
	return 0
}

# Input:
#  1: path to the scsi device in sysfs
# Output variables:
#  SDEV_WWID
function sdev_get_wwid() {
	local sdev="${1}"
	local -a wwid

	declare -g SDEV_WWID=""

	# read the volume identifier without stripping any content
	#
	# XXX: we can read the WWID file, even if the SDEV is currently not
	#      operational (e.g.: due to the path has gone away), as long as
	#      the VPD PG 83 is still cached in the kernel; and the page gets
	#      only released on SDEV device release.
	{ readarray -d "" -t wwid < "${sdev}"/wwid; } 2>/dev/null	\
								|| return 1
	if '@DEBUG@'; then declare -p wwid 1>&2; fi

	# test whether we read something
	#
	# This strips all leading spaces from the beginning of the read WWID
	# (until the first non-space or NUL character), and checks whether the
	# result is empty.
	# Hence, we return early if the WWID consists of only whitespace.
	#
	# XXX: there could be unexpected characters in the returned ID.
	#      `scsi_id` from the udev helpers sanitizes the strings it reads
	#      from the devices, so they can be used in environment variables
	#      without much danger.
	#      But we don't export anything here, so it should be fine.
	[ "${wwid[0]/#*([[:space:]])}" != "" ]			|| return 2

	# shellcheck disable=2034
	SDEV_WWID="${wwid[0]}"
	return 0
}

# Input:
#  1: path to the scsi device in sysfs
# Return Value:
#  == 0: SDEV referenced by `1` in good state
#  != 0: otherwise
function sdev_test_path_state() {
	local sdev="${1}" state zfcp_failed zfcp_in_recovery rport port_state

	sdev="$(readlink -se "${sdev}")"			|| return 1

	{ read -r state _ < "${sdev}"/state; } 2>/dev/null	|| return 2
	{ read -r zfcp_failed _ < "${sdev}"/zfcp_failed; } 2>/dev/null	\
								|| return 3
	{ read -r zfcp_in_recovery _ < "${sdev}"/zfcp_in_recovery; } 2>/dev/null \
								|| return 4

	printf -v rport "%s" "${sdev}"/../../fc_remote_ports/rport-*:*-*
	# e.g.: /sys/devices/css0/0.0.0016/0.0.1740/host0/rport-0:0-1/fc_remote_ports/rport-0:0-1
	[ "${rport}" != "" ]					|| return 5
	{ read -r port_state _ < "${rport}"/port_state; } 2>/dev/null	\
								|| return 6

	if '@DEBUG@'; then
		declare -p state zfcp_failed zfcp_in_recovery port_state 1>&2
	fi

	[ "${state}" = "running" ]				|| return 7
	[ "${zfcp_failed}" = "0" ]				|| return 8
	[ "${zfcp_in_recovery}" = "0" ]				|| return 9
	{ [ "${port_state}" = "Online" ]				\
	  || [ "${port_state}" = "Marginal" ]; }		|| return 10

	return 0
}

# Input:
#  *: all input parameters are used as quoted message
function log_note() {
	logger -p 'daemon.notice' -t 'chreipl-fcp-mpath' "${*}" &>/dev/null
}

# Input:
#  *: all input parameters are used as quoted message
function log_crit() {
	logger -p 'daemon.crit' -t 'chreipl-fcp-mpath' "${*}" &>/dev/null
}

# Input:
#  *: all input parameters are used as quoted message
function log_alert() {
	logger -p 'daemon.alert' -t 'chreipl-fcp-mpath' "${*}" &>/dev/null
}
