Files
rollcall/cmd/rollcall/grub.go
T
Ilker ManapandClaude Opus 5.5 b2ad0210bf
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 40s
GRUB keeps the firmware's text console; phone-home back to HTTP
Lab, Secure Boot VM: the deployed Debian booted under Secure Boot
through the chainload, but its console was garbled after the network
GRUB had switched the display mode; GRUB menus now use the firmware's
text console (terminal_output console), which also suits serial/SOL.

phone-home over HTTPS failed once on that first boot (TLS: bad record
MAC on the region's side; the same request works from the machine
since), and cloud-init spends its 10 tries within milliseconds. Its
content is not secret and its token only works from the machine's
address, so it goes over HTTP again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:51:22 +01:00

150 lines
4.7 KiB
Go

package main
// UEFI machines boot through shim and GRUB, both signed (Debian's
// shim-signed and grub-efi-amd64-signed), so a machine with Secure Boot on
// PXE-boots at all (syslinux.efi is unsigned: the firmware refuses it) and
// GRUB loads only kernels that shim verifies. BIOS machines keep pxelinux.
//
// TFTP root:
//
// bootx64.efi shim (shimx64.efi.signed)
// grubx64.efi GRUB for network boot (grubnetx64.efi.signed)
// grub/grub.cfg picks grub.cfg-<mac>, else grub.cfg-default
// grub/grub.cfg-<mac> per machine, made from its pxelinux file
//
// The rack controller writes the GRUB files from the pxelinux ones it gets,
// so both menus always say the same. Secure Boot checks the kernel; the
// initrd and the squashfs it fetches are not covered by it.
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const grubDispatch = `# generated by rollcall: the machine's own menu, else the one for unknown machines
terminal_output console
set timeout=8
configfile $prefix/grub.cfg-$net_default_mac
configfile $prefix/grub.cfg-default
`
var grubNameRe = regexp.MustCompile(`^grub\.cfg-([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
// grubName is the GRUB file for a pxelinux one: 01-aa-bb-cc-dd-ee-ff -> grub.cfg-aa:bb:cc:dd:ee:ff.
func grubName(pxe string) string {
if !strings.HasPrefix(pxe, "01-") {
return ""
}
n := "grub.cfg-" + strings.ReplaceAll(strings.TrimPrefix(pxe, "01-"), "-", ":")
if !grubNameRe.MatchString(n) {
return ""
}
return n
}
type pxeEntry struct {
label, kernel, initrd, append string
local, poweroff bool
}
// pxelinuxToGrub turns one of rollcall's pxelinux menus into the same GRUB menu.
func pxelinuxToGrub(body string) string {
var entries []*pxeEntry
def, timeout := 0, 5
var cur *pxeEntry
for _, raw := range strings.Split(body, "\n") {
line := strings.TrimSpace(raw)
f := strings.Fields(line)
if len(f) == 0 || strings.HasPrefix(line, "#") {
continue
}
key := strings.ToUpper(f[0])
rest := strings.TrimSpace(line[len(f[0]):])
switch {
case key == "TIMEOUT":
if n, err := strconv.Atoi(rest); err == nil { // tenths of a second
timeout = (n + 9) / 10
}
case key == "LABEL":
cur = &pxeEntry{label: rest}
entries = append(entries, cur)
case cur == nil:
case key == "MENU" && len(f) > 1 && strings.EqualFold(f[1], "LABEL"):
cur.label = strings.TrimSpace(rest[len(f[1]):])
case key == "MENU" && len(f) > 1 && strings.EqualFold(f[1], "DEFAULT"):
def = len(entries) - 1
case key == "KERNEL":
if strings.HasSuffix(rest, "poweroff.c32") {
cur.poweroff = true
} else {
cur.kernel = rest
}
case key == "INITRD":
cur.initrd = rest
case key == "APPEND":
cur.append = rest
case key == "LOCALBOOT":
cur.local = true
}
}
var b strings.Builder
b.WriteString("# generated by rollcall from the pxelinux menu\n")
// the firmware's text console: GRUB does not switch the display mode, so the OS
// it starts gets the firmware's own (OVMF showed a garbled console after a switch),
// and serial/SOL consoles see the menu
b.WriteString("terminal_output console\n")
fmt.Fprintf(&b, "set default=%d\nset timeout=%d\n", def, timeout)
for _, e := range entries {
fmt.Fprintf(&b, "menuentry %s {\n", grubQuote(e.label))
switch {
case e.poweroff:
b.WriteString(" halt\n")
case e.local:
b.WriteString(grubLocalBoot)
case e.kernel != "":
fmt.Fprintf(&b, " linux %s", grubQuote("/"+strings.TrimPrefix(e.kernel, "/")))
for _, w := range strings.Fields(e.append) {
b.WriteString(" " + grubQuote(w))
}
b.WriteString("\n")
if e.initrd != "" {
fmt.Fprintf(&b, " initrd %s\n", grubQuote("/"+strings.TrimPrefix(e.initrd, "/")))
}
default:
b.WriteString(" exit 1\n")
}
b.WriteString("}\n")
}
return b.String()
}
// grubLocalBoot starts the installed OS by chainloading its signed loader
// from the disk's EFI partition (shim, which Secure Boot accepts). Handing
// back to the firmware with "exit" is not enough: firmware then tries its
// other network boot options and ends in its own menu (OVMF does).
const grubLocalBoot = ` insmod part_gpt
insmod fat
insmod chain
for f in /EFI/debian/shimx64.efi /EFI/ubuntu/shimx64.efi /EFI/rocky/shimx64.efi /EFI/almalinux/shimx64.efi /EFI/redhat/shimx64.efi /EFI/centos/shimx64.efi /EFI/opensuse/shim.efi /EFI/sles/shim.efi /EFI/BOOT/BOOTX64.EFI; do
if search --no-floppy --set=root --file $f; then
chainloader $f
boot
fi
done
echo "no EFI loader found on the disks"
sleep 10
exit 1
`
var grubPlain = regexp.MustCompile(`^[A-Za-z0-9_./:=,+@%-]+$`)
// grubQuote makes a word safe in GRUB's script language (no $ expansion, no ;).
func grubQuote(s string) string {
if grubPlain.MatchString(s) {
return s
}
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}