1#!/bin/sh
2# shellcheck disable=SC2154
3#
4# Turn off/on vdevs' enclosure fault LEDs when their pool's state changes.
5#
6# Turn a vdev's fault LED on if it becomes FAULTED, DEGRADED or UNAVAIL.
7# Turn its LED off when it's back ONLINE again.
8#
9# This script run in two basic modes:
10#
11# 1. If $ZEVENT_VDEV_ENC_SYSFS_PATH and $ZEVENT_VDEV_STATE_STR are set, then
12# only set the LED for that particular vdev. This is the case for statechange
13# events and some vdev_* events.
14#
15# 2. If those vars are not set, then check the state of all vdevs in the pool
16# and set the LEDs accordingly.  This is the case for pool_import events.
17#
18# Note that this script requires that your enclosure be supported by the
19# Linux SCSI Enclosure services (SES) driver.  The script will do nothing
20# if you have no enclosure, or if your enclosure isn't supported.
21#
22# Exit codes:
23#   0: enclosure led successfully set
24#   1: enclosure leds not available
25#   2: enclosure leds administratively disabled
26#   3: The led sysfs path passed from ZFS does not exist
27#   4: $ZPOOL not set
28#   5: awk is not installed
29
30[ -f "${ZED_ZEDLET_DIR}/zed.rc" ] && . "${ZED_ZEDLET_DIR}/zed.rc"
31. "${ZED_ZEDLET_DIR}/zed-functions.sh"
32
33if [ ! -d /sys/class/enclosure ] && [ ! -d /sys/bus/pci/slots ] ; then
34	# No JBOD enclosure or NVMe slots
35	exit 1
36fi
37
38if [ "${ZED_USE_ENCLOSURE_LEDS}" != "1" ] ; then
39	exit 2
40fi
41
42zed_check_cmd "$ZPOOL" || exit 4
43zed_check_cmd awk || exit 5
44
45# Global used in set_led debug print
46vdev=""
47
48# check_and_set_led (file, val)
49#
50# Read an enclosure sysfs file, and write it if it's not already set to 'val'
51#
52# Arguments
53#   file: sysfs file to set (like /sys/class/enclosure/0:0:1:0/SLOT 10/fault)
54#   val: value to set it to
55#
56# Return
57#  0 on success, 3 on missing sysfs path
58#
59check_and_set_led()
60{
61	file="$1"
62	val="$2"
63
64	if [ -z "$val" ]; then
65		return 0
66	fi
67
68	if [ ! -e "$file" ] ; then
69		return 3
70	fi
71
72	# If another process is accessing the LED when we attempt to update it,
73	# the update will be lost so retry until the LED actually changes or we
74	# timeout.
75	for _ in 1 2 3 4 5; do
76		# We want to check the current state first, since writing to the
77		# 'fault' entry always causes a SES command, even if the
78		# current state is already what you want.
79		read -r current < "${file}"
80
81		# On some enclosures if you write 1 to fault, and read it back,
82		# it will return 2.  Treat all non-zero values as 1 for
83		# simplicity.
84		if [ "$current" != "0" ] ; then
85			current=1
86		fi
87
88		if [ "$current" != "$val" ] ; then
89			echo "$val" > "$file"
90			zed_log_msg "vdev $vdev set '$file' LED to $val"
91		else
92			break
93		fi
94	done
95}
96
97# Fault LEDs for JBODs and NVMe drives are handled a little differently.
98#
99# On JBODs the fault LED is called 'fault' and on a path like this:
100#
101#   /sys/class/enclosure/0:0:1:0/SLOT 10/fault
102#
103# On NVMe it's called 'attention' and on a path like this:
104#
105#   /sys/bus/pci/slot/0/attention
106#
107# This function returns the full path to the fault LED file for a given
108# enclosure/slot directory.
109#
110path_to_led()
111{
112	dir=$1
113	if [ -f "$dir/fault" ] ; then
114		echo "$dir/fault"
115	elif [ -f "$dir/attention" ] ; then
116		echo "$dir/attention"
117	fi
118}
119
120state_to_val()
121{
122	state="$1"
123	case "$state" in
124		FAULTED|DEGRADED|UNAVAIL|REMOVED)
125			echo 1
126			;;
127		ONLINE)
128			echo 0
129			;;
130		*)
131			echo "invalid state: $state"
132			;;
133	esac
134}
135
136#
137# Given a nvme name like 'nvme0n1', pass back its slot directory
138# like "/sys/bus/pci/slots/0"
139#
140nvme_dev_to_slot()
141{
142	dev="$1"
143
144	# Get the address "0000:01:00.0"
145	read -r address < "/sys/class/block/$dev/device/address"
146
147	find /sys/bus/pci/slots -regex '.*/[0-9]+/address$' | \
148		while read -r sys_addr; do
149			read -r this_address < "$sys_addr"
150
151			# The format of address is a little different between
152			# /sys/class/block/$dev/device/address and
153			# /sys/bus/pci/slots/
154			#
155			# address=           "0000:01:00.0"
156			# this_address =     "0000:01:00"
157			#
158			if echo "$address" | grep -Eq ^"$this_address" ; then
159				echo "${sys_addr%/*}"
160				break
161			fi
162			done
163}
164
165
166# process_pool (pool)
167#
168# Iterate through a pool and set the vdevs' enclosure slot LEDs to
169# those vdevs' state.
170#
171# Arguments
172#   pool:	Pool name.
173#
174# Return
175#  0 on success, 3 on missing sysfs path
176#
177process_pool()
178{
179	pool="$1"
180
181	# The output will be the vdevs only (from "grep '/dev/'"):
182	#
183	#    U45     ONLINE       0     0     0   /dev/sdk          0
184	#    U46     ONLINE       0     0     0   /dev/sdm          0
185	#    U47     ONLINE       0     0     0   /dev/sdn          0
186	#    U50     ONLINE       0     0     0  /dev/sdbn          0
187	#
188	ZPOOL_SCRIPTS_AS_ROOT=1 $ZPOOL status -c upath,fault_led "$pool" | grep '/dev/' | (
189	rc=0
190	while read -r vdev state _ _ _ therest; do
191		# Read out current LED value and path
192		# Get dev name (like 'sda')
193		dev=$(basename "$(echo "$therest" | awk '{print $(NF-1)}')")
194		vdev_enc_sysfs_path=$(realpath "/sys/class/block/$dev/device/enclosure_device"*)
195		if [ ! -d "$vdev_enc_sysfs_path" ] ; then
196			# This is not a JBOD disk, but it could be a PCI NVMe drive
197			vdev_enc_sysfs_path=$(nvme_dev_to_slot "$dev")
198		fi
199
200		current_val=$(echo "$therest" | awk '{print $NF}')
201
202		if [ "$current_val" != "0" ] ; then
203			current_val=1
204		fi
205
206		if [ -z "$vdev_enc_sysfs_path" ] ; then
207			# Skip anything with no sysfs LED entries
208			continue
209		fi
210
211		led_path=$(path_to_led "$vdev_enc_sysfs_path")
212		if [ ! -e "$led_path" ] ; then
213			rc=3
214			zed_log_msg "vdev $vdev '$led_path' doesn't exist"
215			continue
216		fi
217
218		val=$(state_to_val "$state")
219
220		if [ "$current_val" = "$val" ] ; then
221			# LED is already set correctly
222			continue
223		fi
224
225		if ! check_and_set_led "$led_path" "$val"; then
226			rc=3
227		fi
228	done
229	exit "$rc"; )
230}
231
232if [ -n "$ZEVENT_VDEV_ENC_SYSFS_PATH" ] && [ -n "$ZEVENT_VDEV_STATE_STR" ] ; then
233	# Got a statechange for an individual vdev
234	val=$(state_to_val "$ZEVENT_VDEV_STATE_STR")
235	vdev=$(basename "$ZEVENT_VDEV_PATH")
236	ledpath=$(path_to_led "$ZEVENT_VDEV_ENC_SYSFS_PATH")
237	check_and_set_led "$ledpath" "$val"
238else
239	# Process the entire pool
240	poolname=$(zed_guid_to_pool "$ZEVENT_POOL_GUID")
241	process_pool "$poolname"
242fi
243