# 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): # GNU coreutils: # - mktemp # - readlink # - sync # util-linux: # - flock # - logger # Makes use of udev event environment variables: # SEQNUM # (1) expand failed globs to an empty string # (2) extended pattern matching to strip leading/trailing whitespaces shopt -s nullglob extglob # (1) don't overwrite existing files using redirects (e.g.: `>`) set -o noclobber # make sure any state files created are only writeable by the owning user umask 027 # create log if DEBUG is enabled (with Make: D=1) # # Each script importing this library and expecting a debug log to be created # must declare a *trace tag* in a variable `debug_trace_tag`. This is used as # identifier in the log file name. The format is: # # [[:digit:]][[:digit:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]] # \ /\ / # --------\ /-------- -------------------\ /------------------- # \/ \/ # relative position of some unique abbreviation for the script # execution in the name, excluding any common prefix # udev rules if '@DEBUG@' && [ -v debug_trace_tag ] && tlg="$( mktemp -p '@debugoutdir@' \ "chreiplzfcpmp-${debug_trace_tag}-${SEQNUM:-0}.XXXXXXXXXX" \ 2>/dev/null)" then readonly tlg exec >|"${tlg}" 2>&1 set -x set else unset tlg fi declare -gr ID_FILE='@chreiplzfcpmp-id-file@' declare -gr FW_LOCK_FILE='@chreiplzfcpmp-fwlock-file@' declare -gA TRAP_EXIT_FN=() declare -gf trap_exit 1>/dev/null function trap_exit() { local fn for fn in "${TRAP_EXIT_FN[@]}"; do "${fn}" done trap - EXIT } trap trap_exit EXIT # Output variables: # id_file_unlock_exclusive_create() - call to unlock when finished with # critical section # # XXX: `id_file_lock_*` can't be taken recursively function id_file_lock_exclusive_create() { declare -g ID_FILE_LOCK="" # prevent concurrent file creation # # First, open the file defined in ${ID_FILE} for writing; this will # succeed and create the file only if it doesn't exist already. If the # file already exist, the first open attempt will fail and we fall # back to opening it only for reading; this will always succeed if the # file already exists (the reason why the first attempty failed). In # both cases store the corresponding file descriptor in # ${ID_FILE_LOCK}. # # XXX: This should be race free. # open() with O_EXCL... is atomic (we set `noclobber` as shell # option); at least as long as we talk about a local FS. if ! { exec {ID_FILE_LOCK}>"${ID_FILE}"; } 2>/dev/null; then { exec {ID_FILE_LOCK}<"${ID_FILE}"; } 2>/dev/null \ || return 1 fi declare -gf id_file_unlock_exclusive_create 1>/dev/null function id_file_unlock_exclusive_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_create]" } TRAP_EXIT_FN+=( [id_file_unlock_exclusive_create]=id_file_unlock_exclusive_create ) flock --exclusive --timeout 5 "${ID_FILE_LOCK}" || return 2 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 # # XXX: `id_file_lock_*` can't be taken recursively function id_file_lock_shared_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}. { exec {ID_FILE_LOCK}<"${ID_FILE}"; } 2>/dev/null || return 1 declare -gf id_file_unlock_shared_no_create 1>/dev/null function id_file_unlock_shared_no_create() { if [ -v ID_FILE_LOCK ]; then # release file and implicitly the lock, if taken exec {ID_FILE_LOCK}<&- unset ID_FILE_LOCK fi unset "TRAP_EXIT_FN[id_file_unlock_shared_no_create]" } TRAP_EXIT_FN+=( [id_file_unlock_shared_no_create]=id_file_unlock_shared_no_create ) flock --shared --timeout 5 "${ID_FILE_LOCK}" || return 2 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 # # XXX: `firmware_lock_*` can't be taken recursively function firmware_lock_shared() { 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_shared 1>/dev/null function firmware_unlock_shared() { 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_shared]" } TRAP_EXIT_FN+=([firmware_unlock_shared]=firmware_unlock_shared) flock --shared --timeout 5 "${FIRMWARE_LOCK}" || return 2 return 0 } # Output variables: # IPL_TYPE # IPL_BUSID # IPL_WWPN # IPL_LUN function firmware_get_ipl_information() { declare -g IPL_TYPE="" IPL_BUSID="" IPL_WWPN="" IPL_LUN="" # Take lock so we don't see any intermediate state from other helpers # running in parallel firmware_lock_shared || return 5 { read -r IPL_TYPE _ < /sys/firmware/reipl/reipl_type; } 2>/dev/null \ || return 1 { read -r IPL_BUSID _ < /sys/firmware/reipl/fcp/device; } 2>/dev/null \ || return 2 { read -r IPL_WWPN _ < /sys/firmware/reipl/fcp/wwpn; } 2>/dev/null \ || return 3 { read -r IPL_LUN _ < /sys/firmware/reipl/fcp/lun; } 2>/dev/null \ || return 4 firmware_unlock_shared # show read values in debug log if enabled if '@DEBUG@'; then declare -p IPL_TYPE IPL_BUSID IPL_WWPN IPL_LUN 1>&2 fi return 0 } # Input: # 1: absolute canonical path to the scsi device in sysfs, e.g.: # /sys/devices/css0/0.0.0014/0.0.1700/host1/rport-1:0-0/target1:0:0/1:0:0:1075789848 # Output variables: # SDEV_LUN function sdev_get_lun() { local sdev="${1}" sdev_lun_str # bash uses `intmax_t` as width for integer variables, and glibc # defines this either as `long int` on 64 bit systems, or # `long long int` on other. local -i sdev_lun=0 fcp_lun=0 sdev_lun_str="${sdev##*:}" # e.g.: 1075789848 [[ "${sdev_lun_str}" == +([[:digit:]]) ]] || return 1 # "cast" to integer sdev_lun="${sdev_lun_str}" # convert the Linux integer LUN format to the hexadecimal 64 bit T10 # LUN representation format used by many s390x interfaces (( fcp_lun = (((sdev_lun >> 0) & 0xffff) << 48) | (((sdev_lun >> 16) & 0xffff) << 32) | (((sdev_lun >> 32) & 0xffff) << 16) | (((sdev_lun >> 48) & 0xffff) << 0) )) # the '0x' prefix is part of the length printf -v SDEV_LUN "%#018llx" "${fcp_lun}" # show read values in debug log if enabled if '@DEBUG@'; then declare -p SDEV_LUN 1>&2 fi return 0 } # Input: # 1: path to the scsi device in sysfs, e.g.: # /sys/devices/css0/0.0.0014/0.0.1700/host1/rport-1:0-0/target1:0:0/1:0:0:1075789848 # , or a symlink pointing to the scsi device, e.g.: # /sys/class/block/sds/device # Output variables: # SDEV_BUSID # SDEV_WWPN # SDEV_LUN function sdev_get_fcp_addressing() { local sdev="${1}" fcp_lun rport rport_wwpn zfcp_dev declare -g SDEV_BUSID="" SDEV_WWPN="" SDEV_LUN="" sdev="$(readlink -se "${sdev}")" || return 1 # get the LUN for this SDEV # # sets ${SDEV_LUN} sdev_get_lun "${sdev}" || return 2 # get the WWPN of the remote port this SDEV is attached to 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 3 # XXX: This works even if the rport is currently in a bad # state, so e.g. when it has just gone down because of a # cable pull. { read -r rport_wwpn _ < "${rport}"/port_name; } 2>/dev/null \ || return 4 # The Linux kernel doesn't guarantee the same format as in # /sys/firmware/..., so make sure it is the one we expect. [[ "${rport_wwpn}" =~ ^0x[[:xdigit:]]{1,16}$ ]] || return 5 # the '0x' prefix is part of the length printf -v rport_wwpn "%#018llx" "${rport_wwpn}" # get the Device Bus-ID of the device via which this SDEV is attached zfcp_dev="$(readlink -se "${sdev}"/../../../..)" || return 6 # e.g.: /sys/devices/css0/0.0.0016/0.0.1740 zfcp_dev="${zfcp_dev##*/}" # shellcheck disable=2034 SDEV_BUSID="${zfcp_dev}" # shellcheck disable=2034 SDEV_WWPN="${rport_wwpn}" return 0 } # Input: # 1: path to the scsi device in sysfs # Output variables: # SDEV_WWID function sdev_get_wwid() { local sdev="${1}" local -a wwid declare -g SDEV_WWID="" # read the volume identifier without stripping any content # # XXX: we can read the WWID file, even if the SDEV is currently not # operational (e.g.: due to the path has gone away), as long as # the VPD PG 83 is still cached in the kernel; and the page gets # only released on SDEV device release. { readarray -d "" -t wwid < "${sdev}"/wwid; } 2>/dev/null \ || return 1 if '@DEBUG@'; then declare -p wwid 1>&2; fi # test whether we read something # # This strips all leading spaces from the beginning of the read WWID # (until the first non-space or NUL character), and checks whether the # result is empty. # Hence, we return early if the WWID consists of only whitespace. # # XXX: there could be unexpected characters in the returned ID. # `scsi_id` from the udev helpers sanitizes the strings it reads # from the devices, so they can be used in environment variables # without much danger. # But we don't export anything here, so it should be fine. [ "${wwid[0]/#*([[:space:]])}" != "" ] || return 2 # shellcheck disable=2034 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 }