#!/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