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