#!/bin/sh
#
# Turn off/on vdevs' enclosure fault LEDs when their pool's state changes.
#
# Turn a vdev's fault LED on if it becomes FAULTED, DEGRADED or UNAVAIL.
# Turn its LED off when it's back ONLINE again.
#
# This script run in two basic modes:
#
# 1. If $ZEVENT_VDEV_ENC_SYSFS_PATH and $ZEVENT_VDEV_STATE_STR are set, then
# only set the LED for that particular vdev. This is the case for statechange
# events and some vdev_* events.
#
# 2. If those vars are not set, then check the state of all vdevs in the pool
# and set the LEDs accordingly.  This is the case for pool_import events.
#
# Note that this script requires that your enclosure be supported by the
# Linux SCSI Enclosure services (SES) driver.  The script will do nothing
# if you have no enclosure, or if your enclosure isn't supported.
#
# Exit codes:
#   0: enclosure led successfully set
#   1: enclosure leds not available
#   2: enclosure leds administratively disabled
#   3: The led sysfs path passed from ZFS does not exist
#   4: $ZPOOL not set
#   5: awk is not installed

[ -f "${ZED_ZEDLET_DIR}/zed.rc" ] && . "${ZED_ZEDLET_DIR}/zed.rc"
. "${ZED_ZEDLET_DIR}/zed-functions.sh"

if [ ! -d /sys/class/enclosure ] && [ ! -d /sys/bus/pci/slots ] ; then
	# No JBOD enclosure or NVMe slots
	exit 1
fi

if [ "${ZED_USE_ENCLOSURE_LEDS}" != "1" ] ; then
	exit 2
fi

zed_check_cmd "$ZPOOL" || exit 4
zed_check_cmd awk || exit 5

# Global used in set_led debug print
vdev=""

# check_and_set_led (file, val)
#
# Read an enclosure sysfs file, and write it if it's not already set to 'val'
#
# Arguments
#   file: sysfs file to set (like /sys/class/enclosure/0:0:1:0/SLOT 10/fault)
#   val: value to set it to
#
# Return
#  0 on success, 3 on missing sysfs path
#
check_and_set_led()
{
	file="$1"
	val="$2"

	if [ -z "$val" ]; then
		return 0
	fi

	if [ ! -e "$file" ] ; then
		return 3
	fi

	# If another process is accessing the LED when we attempt to update it,
	# the update will be lost so retry until the LED actually changes or we
	# timeout.
	for _ in 1 2 3 4 5; do
		# We want to check the current state first, since writing to the
		# 'fault' entry always causes a SES command, even if the
		# current state is already what you want.
		read -r current < "${file}"

		# On some enclosures if you write 1 to fault, and read it back,
		# it will return 2.  Treat all non-zero values as 1 for
		# simplicity.
		if [ "$current" != "0" ] ; then
			current=1
		fi

		if [ "$current" != "$val" ] ; then
			echo "$val" > "$file"
			zed_log_msg "vdev $vdev set '$file' LED to $val"
		else
			break
		fi
	done
}

# Fault LEDs for JBODs and NVMe drives are handled a little differently.
#
# On JBODs the fault LED is called 'fault' and on a path like this:
#
#   /sys/class/enclosure/0:0:1:0/SLOT 10/fault
#
# On NVMe it's called 'attention' and on a path like this:
#
#   /sys/bus/pci/slot/0/attention
#
# This function returns the full path to the fault LED file for a given
# enclosure/slot directory.
#
path_to_led()
{
	dir=$1
	if [ -f "$dir/fault" ] ; then
		echo "$dir/fault"
	elif [ -f "$dir/attention" ] ; then
		echo "$dir/attention"
	fi
}

state_to_val()
{
	state="$1"
	case "$state" in
		FAULTED|DEGRADED|UNAVAIL)
			echo 1
			;;
		ONLINE)
			echo 0
			;;
	esac
}

#
# Given a nvme name like 'nvme0n1', pass back its slot directory
# like "/sys/bus/pci/slots/0"
#
nvme_dev_to_slot()
{
	dev="$1"

	# Get the address "0000:01:00.0"
	address=$(cat "/sys/class/block/$dev/device/address")

	# For each /sys/bus/pci/slots subdir that is an actual number
	# (rather than weird directories like "1-3/").
	# shellcheck disable=SC2010
	for i in $(ls /sys/bus/pci/slots/ | grep -E "^[0-9]+$") ; do
		this_address=$(cat "/sys/bus/pci/slots/$i/address")

		# The format of address is a little different between
		# /sys/class/block/$dev/device/address and
		# /sys/bus/pci/slots/
		#
		# address=           "0000:01:00.0"
		# this_address =     "0000:01:00"
		#
		if echo "$address" | grep -Eq ^"$this_address" ; then
			echo "/sys/bus/pci/slots/$i"
			break
		fi
	done
}


# process_pool (pool)
#
# Iterate through a pool and set the vdevs' enclosure slot LEDs to
# those vdevs' state.
#
# Arguments
#   pool:	Pool name.
#
# Return
#  0 on success, 3 on missing sysfs path
#
process_pool()
{
	pool="$1"

	# The output will be the vdevs only (from "grep '/dev/'"):
	#
	#    U45     ONLINE       0     0     0   /dev/sdk          0
	#    U46     ONLINE       0     0     0   /dev/sdm          0
	#    U47     ONLINE       0     0     0   /dev/sdn          0
	#    U50     ONLINE       0     0     0  /dev/sdbn          0
	#
	ZPOOL_SCRIPTS_AS_ROOT=1 $ZPOOL status -c upath,fault_led "$pool" | grep '/dev/' | (
	rc=0
	while read -r vdev state _ _ _ therest; do
		# Read out current LED value and path
		# Get dev name (like 'sda')
		dev=$(basename "$(echo "$therest" | awk '{print $(NF-1)}')")
		vdev_enc_sysfs_path=$(realpath "/sys/class/block/$dev/device/enclosure_device"*)
		if [ ! -d "$vdev_enc_sysfs_path" ] ; then
			# This is not a JBOD disk, but it could be a PCI NVMe drive
			vdev_enc_sysfs_path=$(nvme_dev_to_slot "$dev")
		fi

		current_val=$(echo "$therest" | awk '{print $NF}')

		if [ "$current_val" != "0" ] ; then
			current_val=1
		fi

		if [ -z "$vdev_enc_sysfs_path" ] ; then
			# Skip anything with no sysfs LED entries
			continue
		fi

		led_path=$(path_to_led "$vdev_enc_sysfs_path")
		if [ ! -e "$led_path" ] ; then
			rc=3
			zed_log_msg "vdev $vdev '$led_path' doesn't exist"
			continue
		fi

		val=$(state_to_val "$state")

		if [ "$current_val" = "$val" ] ; then
			# LED is already set correctly
			continue
		fi

		if ! check_and_set_led "$led_path" "$val"; then
			rc=3
		fi
	done
	exit "$rc"; )
}

if [ -n "$ZEVENT_VDEV_ENC_SYSFS_PATH" ] && [ -n "$ZEVENT_VDEV_STATE_STR" ] ; then
	# Got a statechange for an individual vdev
	val=$(state_to_val "$ZEVENT_VDEV_STATE_STR")
	vdev=$(basename "$ZEVENT_VDEV_PATH")
	ledpath=$(path_to_led "$ZEVENT_VDEV_ENC_SYSFS_PATH")
	check_and_set_led "$ledpath" "$val"
else
	# Process the entire pool
	poolname=$(zed_guid_to_pool "$ZEVENT_POOL_GUID")
	process_pool "$poolname"
fi
