deploy: one-shot boot without the bootloader's help (terra runs Limine)
`install <config> localhost` assumed systemd-boot. terra's CachyOS boots
Limine, so it stopped at "/boot/loader/entries doesn't exist" — the check
added in e538788 doing its job, but with nowhere to go.
Limine cannot help here at all: `bootctl status` lists it as
`✗ One-shot entry control`, and CachyOS's pacman hooks regenerate
limine.conf regardless. So drop below the bootloader entirely and use the
firmware's own BootNext, pointing at a temporary UEFI entry that
EFI-stub-boots the installer kernel straight off the ESP. That keeps the
property which makes this safe to attempt: BootNext is spent by that one
boot, so a failed try still comes back on the normal bootloader.
- picked at runtime: systemd-boot loader entry when $BOOT/loader/entries
exists, else arm_efi_bootnext(). jupiter/neptun and terra-after-install
keep the systemd-boot path.
- `efibootmgr --create-only`, NOT `--create`: the latter pushes the entry
to the front of BootOrder, which would make a wiped installer the
permanent default if the install died halfway.
- the EFI stub loads initrd= from the volume it was loaded from, so this
mode stages on --print-esp-path rather than --print-boot-path.
- stale entries from an earlier attempt are removed before adding one, and
homelab-auto-install.service deletes the entry as soon as it boots, so
nothing lingers in NVRAM pointing at a reformatted partition.
- label matching is EXACT ("Homelab Installer"); a prefix match would have
deleted this box's Windows or Limine entry.
Verified against terra's real NVRAM (read-only): the label parser picks out
Limine/UEFI OS/Windows by exact name and rejects prefixes, and both branches
run end-to-end under stubs — BootNext mode emits the right --disk/--part,
loader path and initrd= cmdline, systemd-boot mode still writes its entry and
never calls efibootmgr.
README/CLAUDE.md corrected: terra runs Limine, not systemd-boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+105
-11
@@ -132,6 +132,72 @@ disks_backing() {
|
||||
lsblk -rnso NAME,TYPE "$1" 2>/dev/null | awk '$2 == "disk" { print "/dev/" $1 }'
|
||||
}
|
||||
|
||||
# Label of the temporary UEFI boot entry arm_efi_bootnext() creates. Also the
|
||||
# key the ISO uses to delete it again once it has booted (see flake.nix).
|
||||
EFI_LABEL="Homelab Installer"
|
||||
|
||||
# Boot numbers of every UEFI entry with exactly this label, one per line.
|
||||
# efibootmgr prints `Boot0002* Limine<TAB>HD(1,GPT,...)/\EFI\...`, so the
|
||||
# label runs from past the "Boot####* " prefix up to the first TAB.
|
||||
# (Character classes spelled out rather than {4}: mawk predates ERE intervals.)
|
||||
efi_entries_named() {
|
||||
efibootmgr 2>/dev/null | awk -v want="$1" '
|
||||
/^Boot[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/ {
|
||||
num = substr($0, 5, 4)
|
||||
rest = substr($0, 9)
|
||||
sub(/^\*/, "", rest); sub(/^ +/, "", rest)
|
||||
split(rest, parts, "\t")
|
||||
if (parts[1] == want) print num
|
||||
}'
|
||||
}
|
||||
|
||||
# Arm a genuine one-shot boot of the staged installer WITHOUT any help from the
|
||||
# bootloader: create a UEFI boot entry that EFI-stub-boots the kernel straight
|
||||
# off the ESP, and point BootNext at it.
|
||||
#
|
||||
# Needed because "boot this once, then go back to normal" is not something
|
||||
# every bootloader can do. systemd-boot has it; terra's CachyOS runs Limine,
|
||||
# which reports `One-shot entry control: ✗` and has no equivalent, and whose
|
||||
# limine.conf is regenerated by pacman hooks anyway. BootNext is a firmware
|
||||
# feature, so it works underneath all of them — and the firmware clears it
|
||||
# after that one boot, which is what keeps the "a failed attempt still comes
|
||||
# back on the normal bootloader" property that makes this safe to try.
|
||||
arm_efi_bootnext() {
|
||||
local esp="$1" cmdline="$2"
|
||||
local esp_src esp_disk esp_part num n
|
||||
need efibootmgr
|
||||
esp_src="$(findmnt -no SOURCE --nofsroot --target "$esp")" \
|
||||
|| die "couldn't resolve $esp to a device"
|
||||
esp_disk="$(disks_backing "$esp_src" | head -1 || true)"
|
||||
esp_part="$(cat "/sys/class/block/$(basename "$esp_src")/partition" 2>/dev/null || true)"
|
||||
{ [ -n "$esp_disk" ] && [ -n "$esp_part" ]; } \
|
||||
|| die "couldn't work out the disk + partition number of the ESP ($esp -> $esp_src)"
|
||||
|
||||
# Clear anything left by an earlier attempt first, so repeated runs don't
|
||||
# slowly fill NVRAM with dead entries pointing at a wiped partition.
|
||||
for n in $(efi_entries_named "$EFI_LABEL"); do
|
||||
echo ">> removing stale UEFI entry Boot$n ($EFI_LABEL)"
|
||||
efibootmgr -q -B -b "$n"
|
||||
done
|
||||
|
||||
# --create-only, NOT --create: the latter also pushes the entry to the front
|
||||
# of BootOrder, which would make a wiped installer the permanent default if
|
||||
# anything went wrong. This way the entry is reachable through BootNext and
|
||||
# nothing else, i.e. exactly once.
|
||||
#
|
||||
# The EFI stub loads `initrd=` off the volume it was itself loaded from, so
|
||||
# the path is relative to the ESP root and uses backslashes.
|
||||
efibootmgr -q --create-only --disk "$esp_disk" --part "$esp_part" \
|
||||
--label "$EFI_LABEL" \
|
||||
--loader '\homelab-installer\bzImage' \
|
||||
--unicode "initrd=\\homelab-installer\\initrd $cmdline"
|
||||
|
||||
num="$(efi_entries_named "$EFI_LABEL" | head -1)"
|
||||
[ -n "$num" ] || die "efibootmgr did not create a '$EFI_LABEL' entry"
|
||||
efibootmgr -q --bootnext "$num"
|
||||
echo ">> UEFI BootNext -> Boot$num ($EFI_LABEL); BootOrder untouched"
|
||||
}
|
||||
|
||||
# Sets tb / cpio / bbox — the kexec tarball plus the static cpio+gzip that
|
||||
# kexec-run.sh needs on PATH to rebuild its initrd.
|
||||
#
|
||||
@@ -194,15 +260,30 @@ local_install_prepare_and_reboot() {
|
||||
need stat
|
||||
need df
|
||||
|
||||
# Where to stage the installer, and how to make the box boot it exactly once.
|
||||
#
|
||||
# systemd-boot keeps its entries on $BOOT — the XBOOTLDR partition when there
|
||||
# is one, the ESP otherwise — which is not always /boot. Hardcoding /boot on
|
||||
# a box that mounts its ESP elsewhere just creates a directory on the root
|
||||
# filesystem, and then reboots into an entry the firmware never sees.
|
||||
local boot
|
||||
boot="$(bootctl --print-boot-path 2>/dev/null)" \
|
||||
|| die "bootctl couldn't locate the boot partition — is systemd-boot installed here?"
|
||||
[ -d "$boot/loader/entries" ] \
|
||||
|| die "$boot/loader/entries doesn't exist — systemd-boot isn't installed on this box"
|
||||
# filesystem and then reboots into an entry the firmware never sees.
|
||||
#
|
||||
# No systemd-boot (terra's CachyOS runs Limine) means no `bootctl set-oneshot`,
|
||||
# so fall back to the firmware's own BootNext — see arm_efi_bootnext(). That
|
||||
# path EFI-stub-boots the kernel directly, which requires it to sit on the ESP
|
||||
# itself rather than on a separate XBOOTLDR.
|
||||
local boot boot_mode esp
|
||||
esp="$(bootctl --print-esp-path 2>/dev/null)" \
|
||||
|| die "bootctl couldn't locate the ESP — is this box actually UEFI-booted?"
|
||||
boot="$(bootctl --print-boot-path 2>/dev/null || echo "$esp")"
|
||||
if [ -d "$boot/loader/entries" ]; then
|
||||
boot_mode=systemd-boot
|
||||
else
|
||||
boot_mode=efi-bootnext
|
||||
boot="$esp"
|
||||
need efibootmgr
|
||||
echo ">> no systemd-boot entries at $boot/loader/entries — arming the firmware's"
|
||||
echo " own BootNext instead (bootloader in charge here: $(bootctl status 2>/dev/null | awk '/Product:/ {$1=""; print substr($0,2); exit}' || echo unknown))"
|
||||
fi
|
||||
|
||||
# No default/auto-picked location — the wrong disk here is destroyed
|
||||
# mid-install (see the OS-disk check below), so this always asks rather
|
||||
@@ -273,7 +354,7 @@ local_install_prepare_and_reboot() {
|
||||
echo " -> $osdisk_real ** WIPED, unattended, after the reboot **"
|
||||
# Unquoted on purpose: collapses the one-per-line list onto one line.
|
||||
echo " staging: $stagedir (on $(echo $stage_disks))"
|
||||
echo " entry: $boot/loader/entries/homelab-installer.conf (one-shot)"
|
||||
echo " one-shot: $boot_mode"
|
||||
read -rp ">> type 'yes' to build the installer, reboot into it and wipe $osdisk_real: " ok
|
||||
[ "$ok" = yes ] || die "aborted"
|
||||
fi
|
||||
@@ -330,15 +411,28 @@ local_install_prepare_and_reboot() {
|
||||
mnt_point="$(findmnt -no TARGET --target "$stagedir")"
|
||||
iso_relpath="$(printf '/%s/%s' "${stagedir#"$mnt_point"}" homelab-installer.iso | tr -s /)"
|
||||
|
||||
cat >"$boot/loader/entries/homelab-installer.conf" <<EOF
|
||||
# Identical either way — only the mechanism that gets the kernel booted with
|
||||
# it differs.
|
||||
local cmdline
|
||||
cmdline="nohibernate root=fstab loglevel=4 lsm=landlock,yama,bpf findiso=$iso_relpath homelab.install=$config homelab.keypart=$boot_partuuid"
|
||||
|
||||
case "$boot_mode" in
|
||||
systemd-boot)
|
||||
cat >"$boot/loader/entries/homelab-installer.conf" <<EOF
|
||||
title Homelab Installer ($config, findiso)
|
||||
linux /homelab-installer/bzImage
|
||||
initrd /homelab-installer/initrd
|
||||
options nohibernate root=fstab loglevel=4 lsm=landlock,yama,bpf findiso=$iso_relpath homelab.install=$config homelab.keypart=$boot_partuuid
|
||||
options $cmdline
|
||||
EOF
|
||||
bootctl set-oneshot homelab-installer.conf
|
||||
echo ">> systemd-boot one-shot entry armed"
|
||||
;;
|
||||
efi-bootnext)
|
||||
arm_efi_bootnext "$boot" "$cmdline"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ">> one-shot boot into the installer, then rebooting — it will finish this install itself"
|
||||
bootctl set-oneshot homelab-installer.conf
|
||||
echo ">> rebooting into the installer — it will finish this install itself"
|
||||
systemctl reboot
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user