Skip to content

Commit

Permalink
pkg/boot: Add basic support for GRUB default boot entry
Browse files Browse the repository at this point in the history
Add support for the default boot entry when GRUB_DEFAULT=saved and GRUB_ENABLE_BLSCFG=true in /etc/default/grub.
For this case grub.cfg would generate the below line:
set default="${saved_entry}"
and grubenv would contain the value of ${saved_entry}, which will be compared and used as the default boot entry.
Here it only handles specifically for this case.
The default boot entry will be assigned with an incremented linux.BootRank, therefore it will be displayed from the top of the boot menu.

Testing method:
With CentOS 8, set GRUB_DEFAULT=saved and GRUB_ENABLE_BLSCFG=true in /etc/default/grub, and then run 'grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg'.
Use 'grubby --set-default' to set the default boot option, run 'cat /boot/*/grubenv' to check the value after saved_entry is expected.
After reboot, 'boot' command can see the default boot option displayed at the top of the boot menu.

Signed-off-by: Shelly Chang <[email protected]>
  • Loading branch information
ShellyChang110 authored and rminnich committed Jun 12, 2023
1 parent 6daa073 commit 8a23cba
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 18 deletions.
29 changes: 21 additions & 8 deletions pkg/boot/bls/bls.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const (
// This function skips over invalid or unreadable entries in an effort
// to return everything that is bootable. map variables is the parsed result
// from Grub parser that should be used by BLS parser, pass nil if there's none.
func ScanBLSEntries(log ulog.Logger, fsRoot string, variables map[string]string) ([]boot.OSImage, error) {
func ScanBLSEntries(l ulog.Logger, fsRoot string, variables map[string]string, grubDefaultSavedEntry string) ([]boot.OSImage, error) {
entriesDir := filepath.Join(fsRoot, blsEntriesDir)

files, err := filepath.Glob(filepath.Join(entriesDir, "*.conf"))
Expand Down Expand Up @@ -67,9 +67,16 @@ func ScanBLSEntries(log ulog.Logger, fsRoot string, variables map[string]string)
for _, f := range files {
identifier := strings.TrimSuffix(filepath.Base(f), ".conf")

img, err := parseBLSEntry(f, fsRoot, variables)
// If the config file name is the same as the Grub default option, pass true for grubDefaultFlag
var img boot.OSImage
var err error
if strings.Compare(identifier, grubDefaultSavedEntry) == 0 {
img, err = parseBLSEntry(f, fsRoot, variables, true)
} else {
img, err = parseBLSEntry(f, fsRoot, variables, false)
}
if err != nil {
log.Printf("BootLoaderSpec skipping entry %s: %v", f, err)
l.Printf("BootLoaderSpec skipping entry %s: %v", f, err)
continue
}
imgs[identifier] = img
Expand Down Expand Up @@ -163,9 +170,8 @@ func getGrubvalue(variables map[string]string, key string) (string, error) {
return "", nil
}

func parseLinuxImage(vals map[string]string, fsRoot string, variables map[string]string) (boot.OSImage, error) {
func parseLinuxImage(vals map[string]string, fsRoot string, variables map[string]string, grubDefaultFlag bool) (boot.OSImage, error) {
linux := &boot.LinuxImage{}

var cmdlines []string
var tokens []string
var value string
Expand Down Expand Up @@ -242,11 +248,18 @@ func parseLinuxImage(vals map[string]string, fsRoot string, variables map[string
// If both title and version were empty, so will this.
linux.Name = strings.Join(name, " ")
linux.Cmdline = strings.Join(cmdlines, " ")
linux.BootRank = blsDefaultRank
// If this is the default option, increase the BootRank by 1
// when os.LookupEnv("BLS_BOOT_RANK") doesn't exist so it's not affected.
if val, exist := os.LookupEnv("BLS_BOOT_RANK"); exist {
if rank, err := strconv.Atoi(val); err == nil {
linux.BootRank = rank
}
} else {
if grubDefaultFlag {
linux.BootRank = blsDefaultRank + 1
} else {
linux.BootRank = blsDefaultRank
}
}

return linux, nil
Expand All @@ -255,7 +268,7 @@ func parseLinuxImage(vals map[string]string, fsRoot string, variables map[string
// parseBLSEntry takes a Type #1 BLS entry and the directory of entries, and
// returns a LinuxImage.
// An error is returned if the syntax is wrong or required keys are missing.
func parseBLSEntry(entryPath, fsRoot string, variables map[string]string) (boot.OSImage, error) {
func parseBLSEntry(entryPath, fsRoot string, variables map[string]string, grubDefaultFlag bool) (boot.OSImage, error) {
vals, err := parseConf(entryPath)
if err != nil {
return nil, fmt.Errorf("error parsing config in %s: %w", entryPath, err)
Expand All @@ -264,7 +277,7 @@ func parseBLSEntry(entryPath, fsRoot string, variables map[string]string) (boot.
var img boot.OSImage
err = fmt.Errorf("neither linux, efi, nor multiboot present in BootLoaderSpec config")
if _, ok := vals["linux"]; ok {
img, err = parseLinuxImage(vals, fsRoot, variables)
img, err = parseLinuxImage(vals, fsRoot, variables, grubDefaultFlag)
} else if _, ok := vals["multiboot"]; ok {
err = fmt.Errorf("multiboot not yet supported")
} else if _, ok := vals["efi"]; ok {
Expand Down
8 changes: 4 additions & 4 deletions pkg/boot/bls/bls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func DISABLEDTestGenerateConfigs(t *testing.T) {
for _, test := range tests {
configPath := strings.TrimSuffix(test, ".json")
t.Run(configPath, func(t *testing.T) {
imgs, err := ScanBLSEntries(ulogtest.Logger{t}, configPath, nil)
imgs, err := ScanBLSEntries(ulogtest.Logger{t}, configPath, nil, "")
if err != nil {
t.Fatalf("Failed to parse %s: %v", test, err)
}
Expand All @@ -56,7 +56,7 @@ func TestParseBLSEntries(t *testing.T) {

for _, tt := range blsEntries {
t.Run(tt.entry, func(t *testing.T) {
image, err := parseBLSEntry(filepath.Join(dir, tt.entry), fsRoot, nil)
image, err := parseBLSEntry(filepath.Join(dir, tt.entry), fsRoot, nil, false)
if err != nil {
if tt.err == "" {
t.Fatalf("Got error %v", err)
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestScanBLSEntries(t *testing.T) {
t.Errorf("Failed to read test json '%v':%v", test, err)
}

imgs, err := ScanBLSEntries(ulogtest.Logger{t}, configPath, nil)
imgs, err := ScanBLSEntries(ulogtest.Logger{t}, configPath, nil, "")
if err != nil {
t.Fatalf("Failed to parse %s: %v", test, err)
}
Expand All @@ -110,7 +110,7 @@ func TestSetBLSRank(t *testing.T) {

for _, tt := range blsEntries {
t.Run(tt.entry, func(t *testing.T) {
image, err := parseBLSEntry(filepath.Join(dir, tt.entry), fsRoot, nil)
image, err := parseBLSEntry(filepath.Join(dir, tt.entry), fsRoot, nil, false)
if err != nil {
if tt.err == "" {
t.Fatalf("Got error %v", err)
Expand Down
65 changes: 61 additions & 4 deletions pkg/boot/grub/grub.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package grub

import (
"context"
"errors"
"fmt"
"io"
"log"
Expand Down Expand Up @@ -43,6 +44,13 @@ var probeGrubFiles = []string{
"grub2/grub.cfg",
"boot/grub2/grub.cfg",
}
var probeGrubEnvFiles = []string{
"EFI/*/grubenv",
"boot/grub/grubenv",
"grub/grubenv",
"grub2/grubenv",
"boot/grub2/grubenv",
}

// Grub syntax for OpenSUSE/Fedora/RHEL has some undocumented quirks. You
// won't find it on the master branch, but instead look at the rhel and fedora
Expand All @@ -63,6 +71,8 @@ var anyEscape = regexp.MustCompile(`\\.{0,3}`)
// mountFlags are the flags this grub interpreter uses to mount partitions.
var mountFlags = uintptr(mount.ReadOnly)

var errMissingKey = errors.New("key is not found")

// absFileScheme creates a file:/// scheme with an absolute path. Technically,
// file schemes must be absolute paths and Go makes that assumption.
func absFileScheme(path string) (*url.URL, error) {
Expand Down Expand Up @@ -113,11 +123,11 @@ func ParseLocalConfig(ctx context.Context, diskDir string, devices block.BlockDe
return nil, fmt.Errorf("no valid grub config found")
}

func grubScanBLSEntries(mountPool *mount.Pool, variables map[string]string) ([]boot.OSImage, error) {
func grubScanBLSEntries(mountPool *mount.Pool, variables map[string]string, grubDefaultSavedEntry string) ([]boot.OSImage, error) {
var images []boot.OSImage
// Scan each mounted partition for BLS entries
for _, m := range mountPool.MountPoints {
imgs, _ := bls.ScanBLSEntries(ulog.Null, m.Path, variables)
imgs, _ := bls.ScanBLSEntries(ulog.Null, m.Path, variables, grubDefaultSavedEntry)
images = append(images, imgs...)
}
if len(images) == 0 {
Expand All @@ -126,6 +136,29 @@ func grubScanBLSEntries(mountPool *mount.Pool, variables map[string]string) ([]b
return images, nil
}

// Find and return the value of the key from a grubenv file
func findkeywordGrubEnv(file string, fsRoot string, key string) (string, error) {
FileGrubenv, err := filepath.Glob(filepath.Join(fsRoot, file))
if FileGrubenv == nil || err != nil {
return "", err
}
relNamesFile, err := os.Open(FileGrubenv[0])
if err != nil {
return "", fmt.Errorf("[grubenv]:%w", err)
}
grubenv, err := uio.ReadAll(relNamesFile)
if err != nil {
return "", fmt.Errorf("[grubenv]:%w", err)
}
for _, line := range strings.Split(string(grubenv), "\n") {
vals := strings.SplitN(line, "=", 2)
if vals[0] == key {
return vals[1], nil
}
}
return "", fmt.Errorf("%q:%w", key, errMissingKey)
}

// ParseConfigFile parses a grub configuration as specified in
// https://www.gnu.org/software/grub/manual/grub/
//
Expand All @@ -146,16 +179,31 @@ func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, roo
seenLinux := make(map[*boot.LinuxImage]struct{})
seenMB := make(map[*boot.MultibootImage]struct{})

var grubDefaultSavedEntry string
// If the value of keyword "default_saved_entry" exists, find the value of "save_entry" from grubenv files from all possible paths.
if _, ok := p.variables["default_saved_entry"]; ok {
for _, m := range mountPool.MountPoints {
for _, file := range probeGrubEnvFiles {
// Parse grubenv and return the value of 'saved_entry'.
val, _ := findkeywordGrubEnv(file, m.Path, "saved_entry")
if val != "" {
grubDefaultSavedEntry = val
}
}
}
}

if defaultEntry, ok := p.variables["default"]; ok {
p.labelOrder = append([]string{defaultEntry}, p.labelOrder...)
}

var images []boot.OSImage
if p.blscfgFound {
if imgs, err := grubScanBLSEntries(p.mountPool, p.variables); err == nil {
if imgs, err := grubScanBLSEntries(p.mountPool, p.variables, grubDefaultSavedEntry); err == nil {
images = append(images, imgs...)
}
}

for _, label := range p.labelOrder {
if img, ok := p.linuxEntries[label]; ok {
if _, ok := seenLinux[img]; !ok {
Expand Down Expand Up @@ -456,7 +504,16 @@ func (c *parser) append(ctx context.Context, config string) error {
if vals[0] == "root" {
continue
}
c.variables[vals[0]] = vals[1]
// here we only add the support for the case: set default="${saved_entry}".
if vals[0] == "default" {
if vals[1] == "${saved_entry}" {
c.variables["default_saved_entry"] = vals[1]
} else {
c.variables[vals[0]] = vals[1]
}
} else {
c.variables[vals[0]] = vals[1]
}
}

case "configfile":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "testdata_new/CentOS_8_Stream_x86_64_blscfg_sda1/boot/vmlinuz-5.18.0"
},
"name": "CentOS Linux (5.18.0) 8 5.18.0",
"rank": "1"
"rank": "2"
},
{
"cmdline": "root=UUID=d0be5cb9-622d-42d5-a531-674aaa120309 ro crashkernel=auto rhgb quiet console=ttyS1,57600n8",
Expand Down
2 changes: 1 addition & 1 deletion pkg/boot/localboot/localboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (a byRank) Len() int { return len(a) }

// parse treats device as a block device with a file system.
func parse(l ulog.Logger, device *block.BlockDev, devices block.BlockDevices, mountDir string, mountPool *mount.Pool) []boot.OSImage {
imgs, err := bls.ScanBLSEntries(l, mountDir, nil)
imgs, err := bls.ScanBLSEntries(l, mountDir, nil, "")
if err != nil {
l.Printf("No systemd-boot BootLoaderSpec configs found on %s, trying another format...: %v", device, err)
}
Expand Down

0 comments on commit 8a23cba

Please sign in to comment.