init: split initialization into quasi-idempotent parts

This commit is contained in:
Andrew J. Hesford 2024-03-01 14:51:09 -05:00
parent 0b08977e7e
commit e3b1ff3df1
9 changed files with 302 additions and 244 deletions

36
zfsbootmenu/init.d/10-kmods Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
[ "${ZFSBOOTMENU_INITIALIZATION}" = "yes" ] || return 0
# Attempt to load spl normally
if ! _modload="$( modprobe spl 2>&1 )" ; then
zdebug "${_modload}"
# Capture the filename for spl.ko
_modfilename="$( modinfo -F filename spl )"
if [ -n "${_modfilename}" ] ; then
zinfo "loading ${_modfilename}"
# Load with a hostid of 0, so that /etc/hostid takes precedence and
# invalid spl.spl_hostid values are ignored
# There's a race condition between udev and insmod spl
# insmod failures are no longer a hard failure - they can be because
# 1. spl.ko is already loaded because of the race condition
# 2. there's an invalid parameter or value for spl.ko
if ! _modload="$( insmod "${_modfilename}" "spl_hostid=0" 2>&1 )" ; then
zwarn "${_modload}"
zwarn "unable to load SPL kernel module; attempting to load ZFS anyway"
fi
fi
fi
if ! _modload="$( modprobe zfs 2>&1 )" ; then
zerror "${_modload}"
emergency_shell "unable to load ZFS kernel modules"
fi
udevadm settle

21
zfsbootmenu/init.d/20-hostid Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
[ "${ZFSBOOTMENU_INITIALIZATION}" = "yes" ] || return 0
# Write out a default or overridden hostid
if [ -n "${spl_hostid}" ] ; then
if write_hostid "${spl_hostid}" ; then
zinfo "writing /etc/hostid from command line: ${spl_hostid}"
else
# write_hostid logs an error for us, just note the new value
# shellcheck disable=SC2154
write_hostid "${default_hostid}"
zinfo "defaulting hostid to ${default_hostid}"
fi
elif [ ! -e /etc/hostid ]; then
zinfo "no hostid found on kernel command line or /etc/hostid"
# shellcheck disable=SC2154
zinfo "defaulting hostid to ${default_hostid}"
write_hostid "${default_hostid}"
fi

View File

@ -0,0 +1,57 @@
#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
[ "${ZFSBOOTMENU_INITIALIZATION}" = "yes" ] || return 0
# Wait for devices to show up
if [ -n "${zbm_wait_for_devices}" ]; then
IFS=',' read -r -a user_devices <<<"${zbm_wait_for_devices}"
while true; do
FOUND=0
EXPECTED=0
missing=()
for device in "${user_devices[@]}"; do
case "${device}" in
/dev/*)
((EXPECTED=EXPECTED+1))
if [ -e "${device}" ] ; then
((FOUND=FOUND+1))
else
missing+=( "$device" )
fi
;;
*=*)
((EXPECTED=EXPECTED+1))
path_prefix="/dev/disk/by-${device%=*}"
checkfor="${path_prefix,,}/${device##*=}"
if [ -e "${checkfor}" ] ; then
((FOUND=FOUND+1))
else
missing+=( "$device" )
fi
;;
*)
zerror "malformed device: '${device}'"
;;
esac
done
if [ ${FOUND} -eq ${EXPECTED} ]; then
break
else
if ! timed_prompt -d "${zbm_retry_delay:-5}" \
-e "to cancel" -m "" \
-m "$( colorize red "One or more required devices are missing" )" \
-p "retrying in $( colorize yellow "%0.2d" ) seconds" ; then
for dev in "${missing[@]}" ; do
zerror "required device '${dev}' not found"
done
break
fi
fi
done
unset FOUND EXPECTED device path_prefix checkfor
fi

View File

@ -0,0 +1,31 @@
#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
[ "${ZFSBOOTMENU_INITIALIZATION}" = "yes" ] || return 0
# Import ZBM hooks from an external root, if they exist
if [ -n "${zbm_hook_root}" ]; then
import_zbm_hooks "${zbm_hook_root}"
fi
# Remove the executable bit from any hooks in the skip list
if zbm_skip_hooks="$( get_zbm_arg zbm.skip_hooks )" && [ -n "${zbm_skip_hooks}" ]; then
zdebug "processing hook skip directives: ${zbm_skip_hooks}"
IFS=',' read -r -a zbm_skip_hooks <<<"${zbm_skip_hooks}"
for _skip in "${zbm_skip_hooks[@]}"; do
[ -n "${_skip}" ] || continue
for _hook in /libexec/hooks/*.d/*; do
[ -e "${_hook}" ] || continue
if [ "${_skip}" = "${_hook##*/}" ]; then
zinfo "Disabling hook: ${_hook}"
chmod 000 "${_hook}"
fi
done
done
unset _hook _skip
fi
# Run early setup hooks, if they exist
tput clear
/libexec/zfsbootmenu-run-hooks -once "early-setup.d"

View File

@ -0,0 +1,117 @@
#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
[ "${ZFSBOOTMENU_INITIALIZATION}" = "yes" ] || return 0
# If a boot pool is specified, that will be tried first
# shellcheck disable=SC2154
try_pool="${zbm_prefer_pool}"
zbm_import_attempt=0
while true; do
if [ -n "${try_pool}" ]; then
zdebug "attempting to import preferred pool ${try_pool}"
fi
read_write='' import_pool "${try_pool}"
# shellcheck disable=SC2154
if check_for_pools; then
if [ "${zbm_require_pool}" = "only" ]; then
zdebug "only importing ${try_pool}"
break
elif [ -n "${try_pool}" ]; then
# If a single pool was requested and imported, try importing others
try_pool=""
continue
else
# Otherwise, all possible pools were imported, nothing more to try
break
fi
elif [ "${import_policy}" == "hostid" ] && poolmatch="$( match_hostid "${try_pool}" )"; then
zdebug "match_hostid returned: ${poolmatch}"
spl_hostid="${poolmatch##*;}"
export spl_hostid
# Store the hostid to use for for KCL overrides
echo -n "$spl_hostid" > "${BASE}/spl_hostid"
# If match_hostid succeeds, it has imported *a* pool...
if [ -n "${try_pool}" ] && [ "${zbm_require_pool}" = "only" ]; then
# In "only" pool mode, the import was the sole pool desired; nothing more to do
break
else
# Otherwise, try one more pass to pick up other pools matching this hostid
try_pool=""
continue
fi
elif [ -n "${try_pool}" ] && [ -z "${zbm_require_pool}" ]; then
# If a specific pool was tried unsuccessfully but is not a requirement,
# allow another pass to try any other importable pools
try_pool=""
continue
fi
zbm_import_attempt="$((zbm_import_attempt + 1))"
zinfo "unable to import a pool on attempt ${zbm_import_attempt}"
# Just keep retrying after a delay until the user presses ESC
if timed_prompt -d "${zbm_retry_delay:-5}" \
-p "Unable to import $( colorize magenta "${try_pool:-pool}" ), retrying in $( colorize yellow "%0.2d" ) seconds" \
-r "to retry immediately" \
-e "for a recovery shell"; then
continue
fi
log_unimportable
# Allow the user to attempt recovery
emergency_shell "unable to successfully import a pool"
done
# restrict read-write access to any unhealthy pools
while IFS=$'\t' read -r _pool _health; do
if [ "${_health}" != "ONLINE" ]; then
echo "${_pool}" >> "${BASE}/degraded"
zerror "prohibiting read/write operations on ${_pool}"
fi
done <<<"$( zpool list -H -o name,health )"
unset _pool _health
zdebug && zdebug "$( zreport )"
unsupported=0
while IFS=$'\t' read -r _pool _property; do
if [[ "${_property}" =~ "unsupported@" ]]; then
zerror "unsupported property: ${_property}"
if ! grep -q "${_pool}" "${BASE}/degraded" >/dev/null 2>&1 ; then
echo "${_pool}" >> "${BASE}/degraded"
fi
unsupported=1
fi
done <<<"$( zpool get all -H -o name,property )"
if [ "${unsupported}" -ne 0 ]; then
zerror "Unsupported features detected, Upgrade ZFS modules in ZFSBootMenu with generate-zbm"
timed_prompt -m "$( colorize red 'Unsupported features detected')" \
-m "$( colorize red 'Upgrade ZFS modules in ZFSBootMenu with generate-zbm')"
fi
unset unsupported
# Attempt to find the bootfs property
# shellcheck disable=SC2086
while read -r _bootfs; do
if [ "${_bootfs}" = "-" ]; then
BOOTFS=
else
BOOTFS="${_bootfs}"
break
fi
done <<<"$( zpool list -H -o bootfs "${zbm_prefer_pool:---}" )"
unset _bootfs
if [ -n "${BOOTFS}" ]; then
export BOOTFS
echo "${BOOTFS}" > "${BASE}/bootfs"
fi

View File

@ -229,6 +229,10 @@ install_zbm_core() {
done
done
for cfile in "${zfsbootmenu_module_root}/init.d"/*; do
zbm_install_file "${cfile}" "/libexec/init.d/${cfile##*/}" || ret=$?
done
return $ret
}

View File

@ -29,251 +29,33 @@ mkdir -p "${BASE:=/zfsbootmenu}"
# explicitly mount efivarfs as read-only
mount_efivarfs "ro"
# Attempt to load spl normally
if ! _modload="$( modprobe spl 2>&1 )" ; then
zdebug "${_modload}"
# Normalize any forcing variable
case "${ZFSBOOTMENU_FORCE_INIT,,}" in
yes|y|true|t|1) ZFSBOOTMENU_FORCE_INIT=yes;;
*) unset ZFSBOOTMENU_FORCE_INIT;;
esac
# Capture the filename for spl.ko
_modfilename="$( modinfo -F filename spl )"
if [ -n "${_modfilename}" ] ; then
zinfo "loading ${_modfilename}"
# Load with a hostid of 0, so that /etc/hostid takes precedence and
# invalid spl.spl_hostid values are ignored
# There's a race condition between udev and insmod spl
# insmod failures are no longer a hard failure - they can be because
# 1. spl.ko is already loaded because of the race condition
# 2. there's an invalid parameter or value for spl.ko
if ! _modload="$( insmod "${_modfilename}" "spl_hostid=0" 2>&1 )" ; then
zwarn "${_modload}"
zwarn "unable to load SPL kernel module; attempting to load ZFS anyway"
fi
fi
fi
if ! _modload="$( modprobe zfs 2>&1 )" ; then
zerror "${_modload}"
emergency_shell "unable to load ZFS kernel modules"
fi
udevadm settle
# Write out a default or overridden hostid
if [ -n "${spl_hostid}" ] ; then
if write_hostid "${spl_hostid}" ; then
zinfo "writing /etc/hostid from command line: ${spl_hostid}"
else
# write_hostid logs an error for us, just note the new value
# shellcheck disable=SC2154
write_hostid "${default_hostid}"
zinfo "defaulting hostid to ${default_hostid}"
fi
elif [ ! -e /etc/hostid ]; then
zinfo "no hostid found on kernel command line or /etc/hostid"
# shellcheck disable=SC2154
zinfo "defaulting hostid to ${default_hostid}"
write_hostid "${default_hostid}"
fi
# Wait for devices to show up
if [ -n "${zbm_wait_for_devices}" ]; then
IFS=',' read -r -a user_devices <<<"${zbm_wait_for_devices}"
while true; do
FOUND=0
EXPECTED=0
missing=()
for device in "${user_devices[@]}"; do
case "${device}" in
/dev/*)
((EXPECTED=EXPECTED+1))
if [ -e "${device}" ] ; then
((FOUND=FOUND+1))
else
missing+=( "$device" )
fi
;;
*=*)
((EXPECTED=EXPECTED+1))
path_prefix="/dev/disk/by-${device%=*}"
checkfor="${path_prefix,,}/${device##*=}"
if [ -e "${checkfor}" ] ; then
((FOUND=FOUND+1))
else
missing+=( "$device" )
fi
;;
*)
zerror "malformed device: '${device}'"
;;
esac
done
if [ ${FOUND} -eq ${EXPECTED} ]; then
break
else
if ! timed_prompt -d "${zbm_retry_delay:-5}" \
-e "to cancel" -m "" \
-m "$( colorize red "One or more required devices are missing" )" \
-p "retrying in $( colorize yellow "%0.2d" ) seconds" ; then
for dev in "${missing[@]}" ; do
zerror "required device '${dev}' not found"
done
break
fi
fi
done
unset FOUND EXPECTED device path_prefix checkfor
fi
# Import ZBM hooks from an external root, if they exist
if [ -n "${zbm_hook_root}" ]; then
import_zbm_hooks "${zbm_hook_root}"
fi
# Remove the executable bit from any hooks in the skip list
if zbm_skip_hooks="$( get_zbm_arg zbm.skip_hooks )" && [ -n "${zbm_skip_hooks}" ]; then
zdebug "processing hook skip directives: ${zbm_skip_hooks}"
IFS=',' read -r -a zbm_skip_hooks <<<"${zbm_skip_hooks}"
for _skip in "${zbm_skip_hooks[@]}"; do
[ -n "${_skip}" ] || continue
for _hook in /libexec/hooks/*.d/*; do
[ -e "${_hook}" ] || continue
if [ "${_skip}" = "${_hook##*/}" ]; then
zinfo "Disabling hook: ${_hook}"
chmod 000 "${_hook}"
fi
done
done
unset _hook _skip
fi
# Run early setup hooks, if they exist
tput clear
/libexec/zfsbootmenu-run-hooks "early-setup.d"
# If a boot pool is specified, that will be tried first
# shellcheck disable=SC2154
try_pool="${zbm_prefer_pool}"
zbm_import_attempt=0
while true; do
if [ -n "${try_pool}" ]; then
zdebug "attempting to import preferred pool ${try_pool}"
fi
read_write='' import_pool "${try_pool}"
# shellcheck disable=SC2154
if check_for_pools; then
if [ "${zbm_require_pool}" = "only" ]; then
zdebug "only importing ${try_pool}"
break
elif [ -n "${try_pool}" ]; then
# If a single pool was requested and imported, try importing others
try_pool=""
continue
else
# Otherwise, all possible pools were imported, nothing more to try
break
fi
elif [ "${import_policy}" == "hostid" ] && poolmatch="$( match_hostid "${try_pool}" )"; then
zdebug "match_hostid returned: ${poolmatch}"
spl_hostid="${poolmatch##*;}"
export spl_hostid
# Store the hostid to use for for KCL overrides
echo -n "$spl_hostid" > "${BASE}/spl_hostid"
# If match_hostid succeeds, it has imported *a* pool...
if [ -n "${try_pool}" ] && [ "${zbm_require_pool}" = "only" ]; then
# In "only" pool mode, the import was the sole pool desired; nothing more to do
break
else
# Otherwise, try one more pass to pick up other pools matching this hostid
try_pool=""
continue
fi
elif [ -n "${try_pool}" ] && [ -z "${zbm_require_pool}" ]; then
# If a specific pool was tried unsuccessfully but is not a requirement,
# allow another pass to try any other importable pools
try_pool=""
continue
fi
zbm_import_attempt="$((zbm_import_attempt + 1))"
zinfo "unable to import a pool on attempt ${zbm_import_attempt}"
# Just keep retrying after a delay until the user presses ESC
if timed_prompt -d "${zbm_retry_delay:-5}" \
-p "Unable to import $( colorize magenta "${try_pool:-pool}" ), retrying in $( colorize yellow "%0.2d" ) seconds" \
-r "to retry immediately" \
-e "for a recovery shell"; then
continue
fi
log_unimportable
# Allow the user to attempt recovery
emergency_shell "unable to successfully import a pool"
# Run the initializer snippets
for src in /libexec/init.d/*; do
[ -x "${src}" ] || [ -n "${ZFSBOOTMENU_FORCE_INIT}" ] || continue
zinfo "running init stage ${src}"
# shellcheck disable=SC1090
ZFSBOOTMENU_INITIALIZATION=yes source "${src}"
chmod 000 "${src}"
done
# restrict read-write access to any unhealthy pools
while IFS=$'\t' read -r _pool _health; do
if [ "${_health}" != "ONLINE" ]; then
echo "${_pool}" >> "${BASE}/degraded"
zerror "prohibiting read/write operations on ${_pool}"
fi
done <<<"$( zpool list -H -o name,health )"
unset _pool _health
zdebug && zdebug "$( zreport )"
unsupported=0
while IFS=$'\t' read -r _pool _property; do
if [[ "${_property}" =~ "unsupported@" ]]; then
zerror "unsupported property: ${_property}"
if ! grep -q "${_pool}" "${BASE}/degraded" >/dev/null 2>&1 ; then
echo "${_pool}" >> "${BASE}/degraded"
fi
unsupported=1
fi
done <<<"$( zpool get all -H -o name,property )"
if [ "${unsupported}" -ne 0 ]; then
zerror "Unsupported features detected, Upgrade ZFS modules in ZFSBootMenu with generate-zbm"
timed_prompt -m "$( colorize red 'Unsupported features detected')" \
-m "$( colorize red 'Upgrade ZFS modules in ZFSBootMenu with generate-zbm')"
fi
unset unsupported
# Attempt to find the bootfs property
# shellcheck disable=SC2086
while read -r _bootfs; do
if [ "${_bootfs}" = "-" ]; then
BOOTFS=
else
BOOTFS="${_bootfs}"
break
fi
done <<<"$( zpool list -H -o bootfs "${zbm_prefer_pool:---}" )"
unset _bootfs
if [ -n "${BOOTFS}" ]; then
export BOOTFS
echo "${BOOTFS}" > "${BASE}/bootfs"
fi
unset src ZFSBOOTMENU_INITIALIZATION
: > "${BASE}/initialized"
# Finish here unless ZFSBOOTMENU_CONSOLE is set
case "${ZFSBOOTMENU_CONSOLE,,}" in
yes|y|true|t|1) ;;
*) exit 0
esac
unset ZFSBOOTMENU_CONSOLE
# If BOOTFS is not empty display the fast boot menu
# shellcheck disable=SC2154
if [ "${menu_timeout}" -ge 0 ] && [ -n "${BOOTFS}" ]; then
@ -304,10 +86,8 @@ if [ -e "${BASE}/active" ] ; then
emergency_shell "an active instance is already running"
fi
# Otherwise, just continue to launch ZFSBootMenu forever
while true; do
if [ -x /bin/zfsbootmenu ]; then
/bin/zfsbootmenu
fi
[ -x /bin/zfsbootmenu ] && /bin/zfsbootmenu
emergency_shell
done

View File

@ -14,6 +14,12 @@ for src in "${sources[@]}"; do
source "${src}" >/dev/null 2>&1 || exit 1
done
ONE_SHOT_HOOKS=0
if [ "${1}" = "-once" ]; then
ONE_SHOT_HOOKS=1
shift
fi
hook_stage="${1}"
if [ -z "${hook_stage}" ]; then
@ -32,6 +38,11 @@ for _hook in "/libexec/hooks/${hook_stage}"/*; do
zinfo "processing ${_hook}"
"${_hook}"
_ran_hook="yes"
if [ "${ONE_SHOT_HOOKS}" -eq 1 ] >/dev/null 2>&1; then
zinfo "Disabling hook after execution: ${_hook}"
chmod 000 "${_hook}"
fi
done
# Return success if at least one hook ran

View File

@ -42,4 +42,5 @@ EOF
echo "ZFSBootMenu" > /proc/sys/kernel/hostname
# https://busybox.net/FAQ.html#job_control
exec setsid bash -c "exec /libexec/zfsbootmenu-init <${control_term} >${control_term} 2>&1"
ZFSBOOTMENU_CONSOLE=yes exec setsid \
bash -c "exec /libexec/zfsbootmenu-init <${control_term} >${control_term} 2>&1"