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"