Skip to content

Commit

Permalink
Merge pull request #3145 from rear/restore-hybrid-bootloader
Browse files Browse the repository at this point in the history
Support saving and restoring hybrid BIOS/UEFI bootloader setup and clean up bootloader detection
  • Loading branch information
pcahyna committed Feb 16, 2024
2 parents 8c59415 + 40b883c commit ca99d85
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 77 deletions.
58 changes: 26 additions & 32 deletions usr/share/rear/finalize/Linux-i386/630_install_grub.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
# This script is an improvement over the default grub-install '(hd0)'
#
# However the following issues still exist:
# However the following issue still exists:
#
# * We don't know what the first disk will be, so we cannot be sure the MBR
# is written to the correct disk(s). That's why we make all disks bootable.
#
# * There is no guarantee that GRUB was the boot loader used originally.
# One possible attempt would be to save and restore the MBR for each disk,
# but this does not guarantee a correct boot order,
# or even a working boot loader config
# (eg. GRUB stage2 might not be at the exact same location).
# is written to the correct disk(s). That's why we make all suitable disks bootable.

# Skip if another boot loader is already installed
# (then $NOBOOTLOADER is not a true value cf. finalize/default/010_prepare_checks.sh):
is_true $NOBOOTLOADER || return 0

# For UEFI systems with grub legacy with should use efibootmgr instead:
is_true $USING_UEFI_BOOTLOADER && return
# For UEFI systems with grub legacy with should use efibootmgr instead,
# but if BOOTLOADER is explicitly set to GRUB, we are on a hybrid (BIOS/UEFI)
# boot system and we need to install GRUB to MBR as well.
# Therefore, we don't test $USING_UEFI_BOOTLOADER.

# If the BOOTLOADER variable (read by finalize/default/010_prepare_checks.sh)
# is not "GRUB" (which means GRUB Legacy) skip this script (which is only for GRUB Legacy)
Expand All @@ -25,31 +21,27 @@ is_true $USING_UEFI_BOOTLOADER && return
test "GRUB" = "$BOOTLOADER" || return 0

# If the BOOTLOADER variable is "GRUB" (which means GRUB Legacy)
# do not unconditionally trust that because https://github.com/rear/rear/pull/589
# reads (excerpt):
# Problems found:
# The ..._install_grub.sh checked for GRUB2 which is not part
# of the first 2048 bytes of a disk - only GRUB was present -
# thus the check for grub-probe/grub2-probe
# and https://github.com/rear/rear/commit/079de45b3ad8edcf0e3df54ded53fe955abded3b
# reads (excerpt):
# replace grub-install by grub-probe
# as grub-install also exist in legacy grub
# so that it seems there are cases where actually GRUB 2 is used
# but wrongly detected as "GRUB" so that another test is needed
# to detected if actually GRUB 2 is used and that test is to
# check if grub-probe or grub2-probe is installed because
# grub-probe or grub2-probe is only installed in case of GRUB 2
# and when GRUB 2 is installed we assume GRUB 2 is used as boot loader
# so that then we skip this script (which is only for GRUB Legacy)
# because finalize/Linux-i386/660_install_grub2.sh is for installing GRUB 2:
if type -p grub-probe >&2 || type -p grub2-probe >&2 ; then
LogPrint "Skip installing GRUB Legacy boot loader because GRUB 2 is installed (grub-probe or grub2-probe exist)."
# we could in principle trust that and continue because
# layout/save/default/445_guess_bootloader.sh (where the value has been set)
# is now able to distinguish between GRUB Legacy and GRUB 2.
# But, as this code used to support the value "GRUB" for GRUB 2,
# the user can have BOOTLOADER=GRUB set explicitly in the configuration file
# and then it overrides the autodetection in layout/save/default/445_guess_bootloader.sh .
# The user expects this setting to work with GRUB 2, thus for backward compatibility
# we need to take into accout the possibility that GRUB actually means GRUB 2.
if is_grub2_installed ; then
LogPrint "Skip installing GRUB Legacy boot loader because GRUB 2 is installed."
# We have the ErrorIfDeprecated function, but it aborts ReaR by default,
# which is not a good thing to do during recovery.
# Therefore it better to log a warning and continue.
LogPrintError "WARNING: setting BOOTLOADER=GRUB for GRUB 2 is deprecated, set BOOTLOADER=GRUB2 if setting BOOTLOADER explicitly"
return
fi

# The actual work:
LogPrint "Installing GRUB Legacy boot loader:"
# See above for the reasoning why not to use ErrorIfDeprecated
LogPrintError "WARNING: support for GRUB Legacy is deprecated"

# Installing GRUB Legacy boot loader requires an executable "grub":
type -p grub >&2 || Error "Cannot install GRUB Legacy boot loader because there is no 'grub' program."
Expand Down Expand Up @@ -79,8 +71,10 @@ if [[ -r "$LAYOUT_FILE" && -r "$LAYOUT_DEPS" ]] ; then

for disk in $disks ; do
# Installing grub on an LVM PV will wipe the metadata so we skip those
# function is_disk_a_pv returns with 1 if disk is a PV
is_disk_a_pv "$disk" || continue
# function is_disk_a_pv returns true if disk is a PV
is_disk_a_pv "$disk" && continue
# Is the disk suitable for GRUB installation at all?
is_disk_grub_candidate "$disk" || continue
# Use first boot partition by default
part=$( echo $bootparts | cut -d' ' -f1 )

Expand Down
60 changes: 51 additions & 9 deletions usr/share/rear/finalize/Linux-i386/660_install_grub2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
# This script does not check BOOTLOADER because it is also used as fallback
# to install the nowadays most often used bootloader GRUB2
# unless the BOOTLOADER variable tells to install another bootloader
# (other bootloader install scripts check the BOOTLOADER variable).
# (other bootloader install scripts check the BOOTLOADER variable)
# and unless we are using UEFI (BOOTLOADER then indicates the BIOS bootloader
# in a a hybrid boot setup).
#
# This script does not error out because at this late state of "rear recover"
# (i.e. after the backup was restored) I <jsmeix@suse.de> consider it too hard
Expand All @@ -45,19 +47,54 @@
# so that after "rear recover" finished he can manually install the bootloader
# as appropriate for his particular system.

local grub_name
local grub2_install_failed grub2_install_device
local source_disk target_disk junk
local grub2_installed_disks
local part bootparts
local disk disks bootdisk

function bios_grub_install ()
{
local grub2_install_device="$1"

if is_true $USING_UEFI_BOOTLOADER ; then
# If running under UEFI, we need to specify the target explicitly, otherwise grub-install thinks
# that we are installing the EFI bootloader.
if ! chroot $TARGET_FS_ROOT /bin/bash --login -c "$grub_name-install --target=i386-pc $grub2_install_device" ; then
LogPrintError "Failed to install GRUB2 for BIOS boot (target i386-pc) on $bootdisk"
# purely informational test that may help to explain the reason for the error
if ! test -d "$TARGET_FS_ROOT/boot/$grub_name/i386-pc" ; then
LogPrintError "GRUB2 module dir for BIOS boot (boot/$grub_name/i386-pc in $TARGET_FS_ROOT) does not exist, is GRUB2 for BIOS (target i386-pc) installed?"
fi
return 1
fi
else
if ! chroot $TARGET_FS_ROOT /bin/bash --login -c "$grub_name-install $grub2_install_device" ; then
LogPrintError "Failed to install GRUB2 on $grub2_install_device"
return 1
fi
fi
return 0
}

# Skip if another bootloader was already installed:
# In this case NOBOOTLOADER is not true,
# cf. finalize/default/050_prepare_checks.sh
is_true $NOBOOTLOADER || return 0

# For UEFI systems with grub2 we should use efibootmgr instead,
# cf. finalize/Linux-i386/670_run_efibootmgr.sh
is_true $USING_UEFI_BOOTLOADER && return
# but if BOOTLOADER is explicitly set to GRUB2, we are on a hybrid (BIOS/UEFI)
# boot system and we need to install GRUB to MBR as well
if is_true $USING_UEFI_BOOTLOADER && [ "GRUB2" != "$BOOTLOADER" ] ; then
return 0
fi

# Only for GRUB2 - GRUB Legacy will be handled by its own script.
# GRUB2 is detected by testing for grub-probe or grub2-probe which does not exist in GRUB Legacy.
# If neither grub-probe nor grub2-probe is there assume GRUB2 is not there:
type -p grub-probe || type -p grub2-probe || return 0
is_grub2_installed || return 0

LogPrint "Installing GRUB2 boot loader..."

Expand Down Expand Up @@ -101,7 +138,7 @@ if test "$GRUB2_INSTALL_DEVICES" ; then
else
LogPrint "Installing GRUB2 on $grub2_install_device (specified in GRUB2_INSTALL_DEVICES)"
fi
if ! chroot $TARGET_FS_ROOT /bin/bash --login -c "$grub_name-install $grub2_install_device" ; then
if ! bios_grub_install "$grub2_install_device" ; then
LogPrintError "Failed to install GRUB2 on $grub2_install_device"
grub2_install_failed="yes"
fi
Expand Down Expand Up @@ -145,8 +182,8 @@ fi
grub2_installed_disks=()
for disk in $disks ; do
# Installing GRUB2 on an LVM PV will wipe the metadata so we skip those:
# function is_disk_a_pv returns with 1 if disk is a PV
is_disk_a_pv "$disk" || continue
# function is_disk_a_pv returns true if disk is a PV
is_disk_a_pv "$disk" && continue

# Use first boot partition by default:
part=$( echo $bootparts | cut -d' ' -f1 )
Expand All @@ -165,14 +202,16 @@ for disk in $disks ; do

# Install GRUB2 on the boot disk if one was found:
if test "$bootdisk" ; then
# Is the disk suitable for GRUB installation at all?
is_disk_grub_candidate "$bootdisk" || continue
# Continue with the next possible boot disk when GRUB2 was already installed on the current one.
# When there are more disks like /dev/sda and /dev/sdb it can happen that
# for /dev/sda bootdisk=/dev/sda and GRUB2 gets installed on /dev/sda and
# also for /dev/sdb bootdisk=/dev/sda and GRUB2 would get installed again there
# so we avoid that GRUB2 gets needlessly installed two times on the same device:
IsInArray "$bootdisk" "${grub2_installed_disks[@]}" && continue
LogPrint "Found possible boot disk $bootdisk - installing GRUB2 there"
if chroot $TARGET_FS_ROOT /bin/bash --login -c "$grub_name-install $bootdisk" ; then
if bios_grub_install "$bootdisk" ; then
grub2_installed_disks+=( "$bootdisk" )
# In contrast to the above behaviour when GRUB2_INSTALL_DEVICES is specified
# consider it here as a successful bootloader installation when GRUB2
Expand All @@ -181,11 +220,14 @@ for disk in $disks ; do
# Continue with the next possible boot disk:
continue
fi
LogPrintError "Failed to install GRUB2 on possible boot disk $bootdisk"
fi
done

is_true $NOBOOTLOADER || return 0
LogPrintError "Failed to install GRUB2 - you may have to manually install it"
if is_true $USING_UEFI_BOOTLOADER ; then
LogPrintError "Failed to install GRUB2 for BIOS boot - you may have to manually install it to preserve the hybrid BIOS/UEFI boot support, otherwise only UEFI boot will work"
else
LogPrintError "Failed to install GRUB2 - you may have to manually install it"
fi
return 1

10 changes: 9 additions & 1 deletion usr/share/rear/finalize/default/050_prepare_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@
NOBOOTLOADER=1

# Try to read the BOOTLOADER value if /var/lib/rear/recovery/bootloader is not empty.
# Currently (June 2016) the used BOOTLOADER values (grep for '$BOOTLOADER') are:
# Currently (February 2024) the used BOOTLOADER values (grep for '$BOOTLOADER') are:
# GRUB for GRUB Legacy
# GRUB2 for GRUB 2
# ELILO for elilo
# LILO for lilo
# GRUB2-EFI for GRUB 2, EFI version
# EFI for any EFI bootloader, dummy value
# ARM for ARM devices, dummy value
# ARM-ALLWINNER for Allwinner devices
# ZIPL for zIPL, on IBM Z (s390x)
# PPC for any bootloader in the PReP boot partition (can be LILO, YABOOT, GRUB2)

local bootloader_file="$VAR_DIR/recovery/bootloader"
# The output is stored in an artificial bash array so that $BOOTLOADER is the first word:
test -s $bootloader_file && BOOTLOADER=( $( grep -v '^[[:space:]]*#' $bootloader_file ) )
88 changes: 58 additions & 30 deletions usr/share/rear/layout/save/default/445_guess_bootloader.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@

# Determine or guess the used bootloader if not specified by the user
# and save this information into /var/lib/rear/recovery/bootloader
bootloader_file="$VAR_DIR/recovery/bootloader"
local bootloader_file="$VAR_DIR/recovery/bootloader"

local sysconfig_bootloader
local block_device
local blockd
local disk_device
local bootloader_area_strings_file
local block_size
local known_bootloader

# When BOOTLOADER is specified use that:
if test "$BOOTLOADER" ; then
# case-insensitive match, as later we conver all to uppercase
if [[ "$BOOTLOADER" == [Gg][Rr][Uu][Bb] ]] ; then
if is_grub2_installed ; then
LogPrintError "BOOTLOADER=GRUB used to mean GRUB 2 if GRUB 2 is installed and GRUB Legacy if not"
Error "BOOTLOADER set to '$BOOTLOADER', set it to 'GRUB2' explicitly to avoid the ambiguity"
fi
# we should add an ErrorIfDeprecated call here or later for GRUB Legacy deprecation
fi
LogPrint "Using specified bootloader '$BOOTLOADER' for 'rear recover'"
echo "$BOOTLOADER" | tr '[a-z]' '[A-Z]' >$bootloader_file
return
Expand Down Expand Up @@ -57,39 +73,31 @@ for block_device in /sys/block/* ; do
# Continue guessing the used bootloader by inspecting the first bytes on the next disk:
continue
fi
# 'Hah!IdontNeedEFI' is the ASCII representation of the official GUID number
# for a GPT BIOS boot partition which is 21686148-6449-6E6F-744E-656564454649
# see https://en.wikipedia.org/wiki/BIOS_boot_partition (issue #1752).
# Use single quotes for 'Hah!IdontNeedEFI' to be on the safe side
# because with double quotes the ! would cause history expansion if that is enabled
# (non-interactive shells do not perform history expansion by default but better safe than sorry):
if grep -q 'Hah!IdontNeedEFI' $bootloader_area_strings_file ; then
# Because 'Hah!IdontNeedEFI' contains the known bootloader 'EFI'
# the default code below would falsely guess that 'EFI' is used
# but actually another non-EFI bootloader is used here
# cf. https://github.com/rear/rear/issues/1752#issue-303856221
# so that in the 'Hah!IdontNeedEFI' case only non-EFI bootloaders are tested.
# IBM Z (s390) uses zipl boot loader for RHEL and Ubuntu
# cf. https://github.com/rear/rear/issues/2137
for known_bootloader in GRUB2 GRUB ELILO LILO ZIPL ; do
if grep -q -i "$known_bootloader" $bootloader_area_strings_file ; then
LogPrint "Using guessed bootloader '$known_bootloader' for 'rear recover' (found in first bytes on $disk_device with GPT BIOS boot partition)"
echo "$known_bootloader" >$bootloader_file
return
fi
done
# When in the 'Hah!IdontNeedEFI' case no known non-EFI bootloader is found
# continue guessing the used bootloader by inspecting the first bytes on the next disk
# because otherwise the default code below would falsely guess that 'EFI' is used
# cf. https://github.com/rear/rear/pull/1754#issuecomment-383531597
continue
fi
# Check the default cases of known bootloaders.
# IBM Z (s390) uses zipl boot loader for RHEL and Ubuntu
# cf. https://github.com/rear/rear/issues/2137
for known_bootloader in GRUB2-EFI EFI GRUB2 GRUB ELILO LILO ZIPL ; do
for known_bootloader in GRUB2 GRUB LILO ZIPL ; do
if grep -q -i "$known_bootloader" $bootloader_area_strings_file ; then
LogPrint "Using guessed bootloader '$known_bootloader' for 'rear recover' (found in first bytes on $disk_device)"
# If we find "GRUB" (which means GRUB Legacy)
# do not unconditionally trust that because https://github.com/rear/rear/pull/589
# reads (excerpt):
# Problems found:
# The ..._install_grub.sh checked for GRUB2 which is not part
# of the first 2048 bytes of a disk - only GRUB was present -
# thus the check for grub-probe/grub2-probe
# and https://github.com/rear/rear/commit/079de45b3ad8edcf0e3df54ded53fe955abded3b
# reads (excerpt):
# replace grub-install by grub-probe
# as grub-install also exist in legacy grub
# so that if actually GRUB 2 is used, the string in the bootloader area
# is "GRUB" so that another test is needed to detect if actually GRUB 2 is used.
# When GRUB 2 is installed we assume GRUB 2 is used as boot loader.
if [ "$known_bootloader" = "GRUB" ] && is_grub2_installed ; then
known_bootloader=GRUB2
LogPrint "GRUB found in first bytes on $disk_device and GRUB 2 is installed, using GRUB2 as a guessed bootloader for 'rear recover'"
else
LogPrint "Using guessed bootloader '$known_bootloader' for 'rear recover' (found in first bytes on $disk_device)"
fi
echo "$known_bootloader" >$bootloader_file
return
fi
Expand All @@ -103,6 +111,26 @@ for block_device in /sys/block/* ; do
Log "End of strings in the first bytes on $disk_device"
done

# No bootloader detected, but we are using UEFI - there is probably an EFI bootloader
if is_true $USING_UEFI_BOOTLOADER ; then
if is_grub2_installed ; then
echo "GRUB2-EFI" >$bootloader_file
elif test -f /sbin/elilo ; then
echo "ELILO" >$bootloader_file
else
# There is an EFI bootloader, we don't know which one exactly.
# The value "EFI" is a bit redundant with USING_UEFI_BOOTLOADER=1,
# which already indicates that there is an EFI bootloader. We use it as a placeholder
# to not leave $bootloader_file empty.
# Note that it is legal to have USING_UEFI_BOOTLOADER=1 and e.g. known_bootloader=GRUB2
# (i.e. a non=EFI bootloader). This will happen in BIOS/UEFI hybrid boot scenarios.
# known_bootloader=GRUB2 indicates that there is a BIOS bootloader and USING_UEFI_BOOTLOADER=1
# indicates that there is also an EFI bootloader. Only the EFI one is being used at this
# time, but both will need to be restored.
echo "EFI" >$bootloader_file
fi
return 0
fi

# Error out when no bootloader was specified or could be autodetected:
Error "Cannot autodetect what is used as bootloader, see default.conf about 'BOOTLOADER'"
Expand Down
Loading

0 comments on commit ca99d85

Please sign in to comment.