mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
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
(<SDEV>/zfcp_failed, <SDEV>/zfcp_in_recovery), the scsi_transport_fc
port state (<SDEV>/../../fc_remote_ports/rport-*/port_state), and the
scsi device state (<SDEV>/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 <maier@linux.ibm.com>
Signed-off-by: Benjamin Block <bblock@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
448 lines
14 KiB
Bash
448 lines
14 KiB
Bash
#!/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):
|
|
# 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
|
|
}
|