From 3591b94c4220fe019e20cb1e67a1835ce9387b1e Mon Sep 17 00:00:00 2001 From: Benjamin Block Date: Fri, 17 Sep 2021 13:25:35 +0200 Subject: [PATCH] chreipl-fcp-mpath: try to change the re-IPL target if the old one went away MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the final step of the udev rules toolset, we either know that the current event subject is the re-IPL target, a dm-multipath device with the re-IPL target, or represents the same volume based on its WWID. As such it is a candidate to replace the current re-IPL target. The new helper `chreipl-fcp-mpath-try-change-ipl-path` will use the subject itself - in case it is a single scsi disk -, or the dm-multipath device, to test whether there is a path in a good state. "Good state" is based on the zfcp device driver state (/zfcp_failed, /zfcp_in_recovery), the scsi_transport_fc port state (/../../fc_remote_ports/rport-*/port_state), and the scsi device state (/state). A path is only considered if they all indicate a device in good conditions. If such a path is found, the helper will try to set it as new re-IPL target regardless of whether the current re-IPL is still in good shape or not. This is by design, and done to reduce complexity in further state checking, and prevention of races with overlapping events in udev (when executing in parallel workers). Whenever a new re-IPL target is selected and set in /sys/firmware/reipl/fcp/{device,wwpn,lun}, the helper will also update the records in the ID-file - otherwise they might appear as stale, when they aren't. This step in the udev rule processing might also result in log messages written to the syslog (using the utility `logger` from util-linux; writing to /dev/log). In case the re-IPL target is changed, a message with level notice is logged, informing about the new target. In case no good path is found as part of a dm-multipath device, a message with level critical is logged, as it might result in a failed re-IPL if no path is available. Lastly, when changing the information in /sys/firmware/reipl/fcp/ fails for whatever reason, a message with level alert is logged, as the written information might be inconsistent and must be audited/corrected manually by an operator. Reviewed-by: Steffen Maier Signed-off-by: Benjamin Block Signed-off-by: Jan Höppner --- chreipl-fcp-mpath/.gitignore | 1 + chreipl-fcp-mpath/Makefile | 3 +- .../chreipl-fcp-mpath-common.sh.in | 118 +++++++++++++ .../chreipl-fcp-mpath-try-change-ipl-path.in | 158 ++++++++++++++++++ .../udev/rules.d/70-chreipl-fcp-mpath.rules | 4 + 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 chreipl-fcp-mpath/chreipl-fcp-mpath-try-change-ipl-path.in diff --git a/chreipl-fcp-mpath/.gitignore b/chreipl-fcp-mpath/.gitignore index 73304510..0fd34f24 100644 --- a/chreipl-fcp-mpath/.gitignore +++ b/chreipl-fcp-mpath/.gitignore @@ -4,3 +4,4 @@ /chreipl-fcp-mpath-is-ipl-vol /chreipl-fcp-mpath-is-reipl-zfcp /chreipl-fcp-mpath-record-volume-identifier +/chreipl-fcp-mpath-try-change-ipl-path diff --git a/chreipl-fcp-mpath/Makefile b/chreipl-fcp-mpath/Makefile index dee68449..0645d0d2 100644 --- a/chreipl-fcp-mpath/Makefile +++ b/chreipl-fcp-mpath/Makefile @@ -47,7 +47,8 @@ CHREIPL_FCP_MPATH_UDEV_HELPER := \ chreipl-fcp-mpath-is-ipl-tgt \ chreipl-fcp-mpath-is-ipl-vol \ chreipl-fcp-mpath-is-reipl-zfcp \ - chreipl-fcp-mpath-record-volume-identifier + chreipl-fcp-mpath-record-volume-identifier \ + chreipl-fcp-mpath-try-change-ipl-path $(CHREIPL_FCP_MPATH_UDEV_HELPER) $(CHREIPL_FCP_MPATH_COMMON): $(MAKEFILE_LIST) $(CHREIPL_FCP_MPATH_UDEV_HELPER) $(CHREIPL_FCP_MPATH_COMMON): % : %.in diff --git a/chreipl-fcp-mpath/chreipl-fcp-mpath-common.sh.in b/chreipl-fcp-mpath/chreipl-fcp-mpath-common.sh.in index 1b157e3a..83c43614 100644 --- a/chreipl-fcp-mpath/chreipl-fcp-mpath-common.sh.in +++ b/chreipl-fcp-mpath/chreipl-fcp-mpath-common.sh.in @@ -11,6 +11,7 @@ # - sync # util-linux: # - flock +# - logger # Makes use of udev event environment variables: # SEQNUM @@ -112,6 +113,40 @@ function id_file_lock_exclusive_create() { 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 @@ -143,6 +178,36 @@ function id_file_lock_shared_no_create() { 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 # @@ -327,3 +392,56 @@ function sdev_get_wwid() { 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 +} diff --git a/chreipl-fcp-mpath/chreipl-fcp-mpath-try-change-ipl-path.in b/chreipl-fcp-mpath/chreipl-fcp-mpath-try-change-ipl-path.in new file mode 100644 index 00000000..e4516543 --- /dev/null +++ b/chreipl-fcp-mpath/chreipl-fcp-mpath-try-change-ipl-path.in @@ -0,0 +1,158 @@ +#!/bin/bash +# 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): +# Those necessary for sourced library: +# - chreipl-fcp-mpath-common.sh +# GNU coreutils: +# - truncate +# util-linux: +# - hexdump + +# Try to change the current re-IPL target to a dfferent, operational path to +# the same volume. +# +# Makes use of udev event environment variables: +# DM_UUID +# SUBSYSTEM +# DEVPATH +# CHREIPL_FCP_MPATH_IS_TGT + +# shellcheck disable=SC2034 +declare -gr debug_trace_tag=15tcip +# shellcheck disable=SC1091 +source '@chreiplzfcpmp-lib@' || exit 127 + +function apply_ipl_information() { + local sdev_wwid="${1}" + local sdev_busid="${2}" sdev_wwpn="${3}" sdev_lun="${4}" + local ipl_type="${5}" ipl_busid="${6}" ipl_wwpn="${7}" ipl_lun="${8}" + local -a records + local try_update_id_file=true + + [ "${ipl_type}" = "fcp" ] || return 1 + + [[ "${sdev_busid}" =~ ^[[:xdigit:]]{1,3}\.[[:xdigit:]]\.[[:xdigit:]]{1,4}$ ]] \ + || return 2 + [[ "${sdev_wwpn}" =~ ^0x[[:xdigit:]]{16}$ ]] || return 3 + [[ "${sdev_lun}" =~ ^0x[[:xdigit:]]{16}$ ]] || return 4 + + # After updating the firmware re-IPL information below we also try to + # update the information stored in the ID file (necessary, so it + # contains the correct Device-Bus-ID/WWPN/LUN after the update). For + # the update of the ID file we try to grab an exclusive lock, so there + # are no overlapping reads/writes. + # + # In case we can't get the lock because the ID file is missing, but we + # have a direct TGT match, we may still try to change the re-IPL + # information, but skip the ID file update. + # + # "direct match" means, the event subject is either the SDEV that is + # currently set as re-IPL target, or it is the dm-multipath device that + # currently contains the re-IPL target. + if ! id_file_lock_exclusive_no_create; then + # rc == 1 --> could not read ${ID_FILE} + [ "${PIPESTATUS[0]}" -eq 1 ] || return 5 + # if true, we are dealing with a direct TGT match + [ "${CHREIPL_FCP_MPATH_IS_TGT}" = "true" ] || return 6 + try_update_id_file=false + fi + + # If we have a direct match (see in the comment above), we know + # that we have a path to the current re-IPL volume - no matter of the + # WWID. Otherwise, we got here by comparing the WWID of the event + # subject with the one recorded in the ID file; in this case we try to + # make sure the information is still up-to-date. + if [ "${CHREIPL_FCP_MPATH_IS_TGT}" != "true" ]; then + # last bail to make sure we don't overwrite user choices.. + # + # XXX: this will *NOT* prevent the race completely, but at least + # make it less likely + { readarray -d "" -t -u "${ID_FILE_LOCK}" records; } 2>/dev/null \ + || return 7 + if '@DEBUG@'; then declare -p records 1>&2; fi + + [ "${#records[@]}" = "4" ] || return 8 + [ "${records[0]}" = "${sdev_wwid}" ] || return 9 + [ "${records[1]}" = "${ipl_busid}" ] || return 10 + [ "${records[2]}" = "${ipl_wwpn}" ] || return 11 + [ "${records[3]}" = "${ipl_lun}" ] || return 12 + fi + + # Take lock so we don't see any intermediate state from other helpers + # running in parallel + firmware_lock_exclusive || return 13 + + if ! { echo "${sdev_busid}" >| /sys/firmware/reipl/fcp/device \ + && echo "${sdev_wwpn}" >| /sys/firmware/reipl/fcp/wwpn \ + && echo "${sdev_lun}" >| /sys/firmware/reipl/fcp/lun; }; + then + log_alert "Changing the re-IPL device failed. The current re-IPL settings might be inconsistent. Check and correct the settings (see the README.md of chreipl-fcp-mpath) to make sure that the current re-IPL device is valid." + return 14 + fi + + firmware_unlock_exclusive + + if [ "${sdev_busid}" != "${ipl_busid}" ] \ + || [ "${sdev_wwpn}" != "${ipl_wwpn}" ] \ + || [ "${sdev_lun}" != "${ipl_lun}" ]; then + log_note "Changed re-IPL path to: ${sdev_busid}:${sdev_wwpn}:${sdev_lun}." + fi + + # Try to update the information in the ID file if we have gotten the + # lock for it. + if ${try_update_id_file}; then + # reset ID without removing the file + truncate --no-create --size=0 "${ID_FILE}" || return 15 + echo -ne "${sdev_wwid}\x00${sdev_busid}\x00${sdev_wwpn}\x00${sdev_lun}\x00" \ + >>"${ID_FILE}" || return 16 + + id_file_unlock_exclusive_no_create + + if '@DEBUG@'; then hexdump -vC "${ID_FILE}" 1>&2; fi + fi + + return 0 +} + +declare -g SDEV="" +if [[ "${DM_UUID}" == mpath-* ]]; then + # Assume Multipath Device Mapper Device; + # e.g.: DEVPATH = /devices/virtual/block/dm-0 + + for sdev in /sys/"${DEVPATH}"/slaves/sd*/device; do + if sdev_test_path_state "${sdev}"; then + SDEV="${sdev}" + break + fi + done + + # No path of the multipath-device that represents the IPL volume is + # online. + if [ "${SDEV}" = "" ]; then + log_crit "The re-IPL device cannot be changed because no operational path to the re-IPL volume remains. The next re-IPL might fail unless you re-attach or enable at least one valid path to the re-IPL volume." + fi + +elif [ "${SUBSYSTEM}" = block ]; then + # Assume SCSI Disk; + # e.g.: DEVPATH = /devices/css0/0.0.0014/0.0.1700/host0/rport-0:0-0/target0:0:0/0:0:0:1074806808/block/sds + if sdev_test_path_state /sys/"${DEVPATH}"/device; then + SDEV=/sys/"${DEVPATH}"/device + fi + +fi +[ "${SDEV}" != "" ] || exit 0 + +sdev_get_wwid "${SDEV}" || exit 0 +sdev_get_fcp_addressing "${SDEV}" || exit 0 +firmware_get_ipl_information || exit 0 + +# shellcheck disable=SC2153 +apply_ipl_information \ + "${SDEV_WWID}" "${SDEV_BUSID}" "${SDEV_WWPN}" "${SDEV_LUN}" \ + "${IPL_TYPE}" "${IPL_BUSID}" "${IPL_WWPN}" "${IPL_LUN}" || exit 0 + +exit 0 diff --git a/chreipl-fcp-mpath/udev/rules.d/70-chreipl-fcp-mpath.rules b/chreipl-fcp-mpath/udev/rules.d/70-chreipl-fcp-mpath.rules index 05f74c05..3c44992c 100644 --- a/chreipl-fcp-mpath/udev/rules.d/70-chreipl-fcp-mpath.rules +++ b/chreipl-fcp-mpath/udev/rules.d/70-chreipl-fcp-mpath.rules @@ -92,6 +92,10 @@ LABEL="chreipl_fcp_mpath_not_direct_match" # know whether that stayed the same when the change was done. PROGRAM!="chreipl-fcp-mpath-is-ipl-vol", GOTO="chreipl_fcp_mpath_end" +# We are here because of scenarios: +# (A) (a)/(b), +# (B) (a)/(b)/(c)/(d) LABEL="chreipl_fcp_mpath_try_change_ipl_path" +RUN{program}+="chreipl-fcp-mpath-try-change-ipl-path" LABEL="chreipl_fcp_mpath_end"