From e3b1ff3df1e004a644e3b26a67f06998c9cacc3a Mon Sep 17 00:00:00 2001 From: "Andrew J. Hesford" Date: Fri, 1 Mar 2024 14:51:09 -0500 Subject: [PATCH] init: split initialization into quasi-idempotent parts --- zfsbootmenu/init.d/10-kmods | 36 +++ zfsbootmenu/init.d/20-hostid | 21 ++ zfsbootmenu/init.d/30-device-wait | 57 +++++ zfsbootmenu/init.d/40-early-hooks | 31 +++ zfsbootmenu/init.d/50-import-pools | 117 +++++++++ zfsbootmenu/install-helpers.sh | 4 + zfsbootmenu/libexec/zfsbootmenu-init | 266 ++------------------ zfsbootmenu/libexec/zfsbootmenu-run-hooks | 11 + zfsbootmenu/pre-init/zfsbootmenu-preinit.sh | 3 +- 9 files changed, 302 insertions(+), 244 deletions(-) create mode 100755 zfsbootmenu/init.d/10-kmods create mode 100755 zfsbootmenu/init.d/20-hostid create mode 100755 zfsbootmenu/init.d/30-device-wait create mode 100755 zfsbootmenu/init.d/40-early-hooks create mode 100755 zfsbootmenu/init.d/50-import-pools diff --git a/zfsbootmenu/init.d/10-kmods b/zfsbootmenu/init.d/10-kmods new file mode 100755 index 0000000..f314907 --- /dev/null +++ b/zfsbootmenu/init.d/10-kmods @@ -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 diff --git a/zfsbootmenu/init.d/20-hostid b/zfsbootmenu/init.d/20-hostid new file mode 100755 index 0000000..4d5e319 --- /dev/null +++ b/zfsbootmenu/init.d/20-hostid @@ -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 diff --git a/zfsbootmenu/init.d/30-device-wait b/zfsbootmenu/init.d/30-device-wait new file mode 100755 index 0000000..a0ee1c8 --- /dev/null +++ b/zfsbootmenu/init.d/30-device-wait @@ -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 diff --git a/zfsbootmenu/init.d/40-early-hooks b/zfsbootmenu/init.d/40-early-hooks new file mode 100755 index 0000000..403e433 --- /dev/null +++ b/zfsbootmenu/init.d/40-early-hooks @@ -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" diff --git a/zfsbootmenu/init.d/50-import-pools b/zfsbootmenu/init.d/50-import-pools new file mode 100755 index 0000000..5ce113d --- /dev/null +++ b/zfsbootmenu/init.d/50-import-pools @@ -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 diff --git a/zfsbootmenu/install-helpers.sh b/zfsbootmenu/install-helpers.sh index 8eb1ef4..8a23084 100644 --- a/zfsbootmenu/install-helpers.sh +++ b/zfsbootmenu/install-helpers.sh @@ -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 } diff --git a/zfsbootmenu/libexec/zfsbootmenu-init b/zfsbootmenu/libexec/zfsbootmenu-init index b346f3c..3159cd0 100755 --- a/zfsbootmenu/libexec/zfsbootmenu-init +++ b/zfsbootmenu/libexec/zfsbootmenu-init @@ -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 diff --git a/zfsbootmenu/libexec/zfsbootmenu-run-hooks b/zfsbootmenu/libexec/zfsbootmenu-run-hooks index 634ea50..8dc2718 100755 --- a/zfsbootmenu/libexec/zfsbootmenu-run-hooks +++ b/zfsbootmenu/libexec/zfsbootmenu-run-hooks @@ -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 diff --git a/zfsbootmenu/pre-init/zfsbootmenu-preinit.sh b/zfsbootmenu/pre-init/zfsbootmenu-preinit.sh index 37f28db..2047039 100755 --- a/zfsbootmenu/pre-init/zfsbootmenu-preinit.sh +++ b/zfsbootmenu/pre-init/zfsbootmenu-preinit.sh @@ -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"