elemental-toolkit/pkg/bootloader/grub.go

466 lines
14 KiB
Go

/*
Copyright © 2022 - 2025 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bootloader
import (
"bytes"
"fmt"
"path/filepath"
"regexp"
"github.com/rancher/elemental-toolkit/v2/pkg/constants"
"github.com/rancher/elemental-toolkit/v2/pkg/elemental"
"github.com/rancher/elemental-toolkit/v2/pkg/types"
"github.com/rancher/elemental-toolkit/v2/pkg/utils"
efilib "github.com/canonical/go-efilib"
eleefi "github.com/rancher/elemental-toolkit/v2/pkg/efi"
)
const (
grubCfgFile = "grub.cfg"
)
var (
defaultGrubPrefixes = []string{constants.FallbackEFIPath, constants.EntryEFIPath}
)
func getGModulePatterns(module string) []string {
var patterns []string
for _, pattern := range constants.GetDefaultGrubModulesPatterns() {
patterns = append(patterns, filepath.Join(pattern, module))
}
return patterns
}
type Grub struct {
logger types.Logger
fs types.FS
runner types.Runner
platform *types.Platform
shimImg string
grubEfiImg string
mokMngr string
grubPrefixes []string
configFile string
elementalCfg string
legacyElementalCfg string
disableBootEntry bool
clearBootEntry bool
secureBoot bool
}
var _ types.Bootloader = (*Grub)(nil)
type GrubOptions func(g *Grub) error
func NewGrub(cfg *types.Config, opts ...GrubOptions) *Grub {
secureBoot := true
if cfg.Platform.Arch == constants.ArchRiscV64 {
// There is no secure boot for riscv64 for the time being (Dec 2023)
secureBoot = false
}
g := &Grub{
fs: cfg.Fs,
logger: cfg.Logger,
runner: cfg.Runner,
platform: cfg.Platform,
configFile: grubCfgFile,
grubPrefixes: defaultGrubPrefixes,
elementalCfg: filepath.Join(constants.GrubCfgPath, constants.GrubCfg),
legacyElementalCfg: filepath.Join(constants.LegacyGrubCfgPath, constants.GrubCfg),
clearBootEntry: true,
secureBoot: secureBoot,
}
for _, o := range opts {
err := o(g)
if err != nil {
g.logger.Errorf("error applying config option: %s", err.Error())
return nil
}
}
return g
}
func WithSecureBoot(secureboot bool) func(g *Grub) error {
return func(g *Grub) error {
g.secureBoot = secureboot
return nil
}
}
func WithGrubPrefixes(prefixes ...string) func(g *Grub) error {
return func(g *Grub) error {
g.grubPrefixes = prefixes
return nil
}
}
func WithGrubDisableBootEntry(disableBootEntry bool) func(g *Grub) error {
return func(g *Grub) error {
g.disableBootEntry = disableBootEntry
return nil
}
}
func WithGrubAutoDisableBootEntry() func(g *Grub) error {
return func(g *Grub) error {
if g.disableBootEntry {
// already disabled manually, doing nothing
return nil
}
rw, err := elemental.IsRWMountPoint(g.runner, constants.EfivarsMountPath)
if err != nil {
g.logger.Errorf("error finding efivar mounts: %s", err.Error())
return err
}
// If efivars is not RW, disable writing boot entries.
if !rw {
g.disableBootEntry = true
}
return nil
}
}
func WithGrubClearBootEntry(clearBootEntry bool) func(g *Grub) error {
return func(g *Grub) error {
g.clearBootEntry = clearBootEntry
return nil
}
}
func (g *Grub) findEFIImages(rootDir string) error {
var err error
if g.secureBoot && g.shimImg == "" {
g.shimImg, err = utils.FindFile(g.fs, rootDir, constants.GetShimFilePatterns()...)
if err != nil {
g.logger.Errorf("failed to find shim image")
return err
}
}
if g.grubEfiImg == "" {
g.grubEfiImg, err = utils.FindFile(g.fs, rootDir, constants.GetGrubEFIFilePatterns()...)
if err != nil {
g.logger.Errorf("failed to find grub image")
return err
}
}
if g.secureBoot && g.mokMngr == "" {
g.mokMngr, err = utils.FindFile(g.fs, rootDir, constants.GetMokMngrFilePatterns()...)
if err != nil {
g.logger.Errorf("failed to find mok manager")
return err
}
}
return nil
}
func (g *Grub) findModules(rootDir string, modules ...string) ([]string, error) {
fModules := []string{}
for _, module := range modules {
foundModule, err := utils.FindFile(g.fs, rootDir, getGModulePatterns(module)...)
if err != nil {
return []string{}, err
}
fModules = append(fModules, foundModule)
}
return fModules, nil
}
func (g *Grub) installModules(rootDir, bootDir string, modules ...string) error {
modules, err := g.findModules(rootDir, modules...)
if err != nil {
return err
}
for _, grubPrefix := range g.grubPrefixes {
for _, module := range modules {
fileWriteName := filepath.Join(bootDir, grubPrefix, fmt.Sprintf("%s-efi", g.platform.Arch), filepath.Base(module))
g.logger.Debugf("Copying %s to %s", module, fileWriteName)
err = utils.MkdirAll(g.fs, filepath.Dir(fileWriteName), constants.DirPerm)
if err != nil {
return fmt.Errorf("error creating destination folder: %v", err)
}
err = utils.CopyFile(g.fs, module, fileWriteName)
if err != nil {
return fmt.Errorf("error copying %s to %s: %s", module, fileWriteName, err.Error())
}
}
}
return nil
}
func (g *Grub) InstallEFI(rootDir, efiDir string) error {
err := g.installModules(rootDir, efiDir, constants.GetDefaultGrubModules()...)
if err != nil {
return err
}
for _, prefix := range g.grubPrefixes {
err = g.InstallEFIBinaries(rootDir, efiDir, prefix)
if err != nil {
return err
}
}
return nil
}
func (g *Grub) InstallEFIBinaries(rootDir, efiDir, prefix string) error {
err := g.findEFIImages(rootDir)
if err != nil {
return err
}
installPath := filepath.Join(efiDir, prefix)
err = utils.MkdirAll(g.fs, installPath, constants.DirPerm)
if err != nil {
g.logger.Errorf("Error creating dirs: %s", err)
return err
}
shimImg := filepath.Join(installPath, filepath.Base(g.shimImg))
grubEfi := filepath.Join(installPath, filepath.Base(g.grubEfiImg))
var bootImg string
if prefix == constants.FallbackEFIPath {
switch g.platform.Arch {
case constants.ArchAmd64, constants.Archx86:
bootImg = filepath.Join(installPath, constants.EfiImgX86)
case constants.ArchArm64:
bootImg = filepath.Join(installPath, constants.EfiImgArm64)
case constants.ArchRiscV64:
bootImg = filepath.Join(installPath, constants.EfiImgRiscv64)
default:
err = fmt.Errorf("Not supported architecture: %v", g.platform.Arch)
}
if err != nil {
return err
}
if g.secureBoot {
shimImg = bootImg
} else {
grubEfi = bootImg
}
}
if g.secureBoot {
g.logger.Debugf("Copying %s to %s", g.mokMngr, installPath)
err = utils.CopyFile(g.fs, g.mokMngr, installPath)
if err != nil {
return fmt.Errorf("failed copying %s to %s: %s", g.mokMngr, installPath, err.Error())
}
g.logger.Debugf("Copying %s to %s", g.shimImg, shimImg)
err = utils.CopyFile(g.fs, g.shimImg, shimImg)
if err != nil {
return fmt.Errorf("failed copying %s to %s: %s", g.shimImg, shimImg, err.Error())
}
}
g.logger.Debugf("Copying %s to %s", g.grubEfiImg, grubEfi)
err = utils.CopyFile(g.fs, g.grubEfiImg, grubEfi)
if err != nil {
return fmt.Errorf("failed copying %s to %s: %s", g.grubEfiImg, installPath, err.Error())
}
return nil
}
// DoEFIEntries creates clears any previous entry if requested and creates a new one with the given shim name.
func (g *Grub) DoEFIEntries(shimName, efiDir string) error {
efivars := eleefi.RealEFIVariables{}
if g.clearBootEntry {
err := g.clearEntry(efivars)
if err != nil {
return err
}
}
return g.CreateEntry(shimName, filepath.Join(efiDir, constants.EntryEFIPath), efivars)
}
// clearEntry will go over the BootXXXX efi vars and remove any that matches our name
// Used in install as we re-create the partitions, so the UUID of those partitions is no longer valid for the old entry
// And we don't want to leave a broken entry around
func (g *Grub) clearEntry(efivars eleefi.Variables) error {
variables, _ := efivars.ListVariables()
for _, v := range variables {
if regexp.MustCompile(`Boot[0-9a-fA-F]{4}`).MatchString(v.Name) {
variable, _, _ := efivars.GetVariable(v.GUID, v.Name)
option, err := efilib.ReadLoadOption(bytes.NewReader(variable))
if err != nil {
continue
}
// TODO: Find a way to identify the old VS new partition UUID and compare them before removing?
if option.Description == constants.BootEntryName {
g.logger.Debugf("Entry for %s already exists, removing it: %s", constants.BootEntryName, option.String())
_, attrs, err := efivars.GetVariable(v.GUID, v.Name)
if err != nil {
g.logger.Errorf("failed to remove efi entry %s: %s", v.Name, err.Error())
return err
}
err = efivars.SetVariable(v.GUID, v.Name, nil, attrs)
if err != nil {
g.logger.Errorf("failed to remove efi entry %s: %s", v.Name, err.Error())
return err
}
}
}
}
return nil
}
// createBootEntry will create an entry in the efi vars for our shim and set it to boot first in the bootorder
func (g *Grub) CreateEntry(shimName string, relativeTo string, efiVariables eleefi.Variables) error {
g.logger.Debugf("Creating boot entry for elemental pointing to shim %s/%s", constants.EntryEFIPath, shimName)
bm, err := eleefi.NewBootManagerForVariables(g.logger, efiVariables)
if err != nil {
return err
}
// HINT: FindOrCreate does not find older entries if the partition UUID has changed, i.e. on a reinstall.
bootEntryNumber, err := bm.FindOrCreateEntry(eleefi.BootEntry{
Filename: shimName,
Label: constants.BootEntryName,
Description: constants.BootEntryName,
}, relativeTo)
if err != nil {
g.logger.Errorf("error creating boot entry: %s", err.Error())
return err
}
// Commit the new boot order by prepending our entry to the current boot order
err = bm.PrependAndSetBootOrder([]int{bootEntryNumber})
if err != nil {
g.logger.Errorf("error setting boot order: %s", err.Error())
return err
}
g.logger.Infof("Entry created for %s in the EFI boot manager", constants.BootEntryName)
return nil
}
// Sets the given key value pairs into as grub variables into the given file
func (g *Grub) SetPersistentVariables(grubEnvFile string, vars map[string]string) error {
cmd := "grub2-editenv"
if !g.runner.CommandExists(cmd) {
cmd = "grub-editenv"
}
for key, value := range vars {
g.logger.Debugf("Running %s with params: %s set %s=%s", cmd, grubEnvFile, key, value)
out, err := g.runner.Run(cmd, grubEnvFile, "set", fmt.Sprintf("%s=%s", key, value))
if err != nil {
g.logger.Errorf(fmt.Sprintf("Failed setting grub variables: %s", out))
return err
}
}
return nil
}
// SetDefaultEntry Sets the default_meny_entry value in RunConfig.GrubOEMEnv file at in
// State partition mountpoint. If there is not a custom value in the os-release file, we do nothing
// As the grub config already has a sane default
func (g *Grub) SetDefaultEntry(partMountPoint, imgMountPoint, defaultEntry string) error {
var configEntry string
osRelease, err := utils.LoadEnvFile(g.fs, filepath.Join(imgMountPoint, "etc", "os-release"))
g.logger.Debugf("Looking for GRUB_ENTRY_NAME name in %s", filepath.Join(imgMountPoint, "etc", "os-release"))
if err != nil {
g.logger.Warnf("Could not load os-release file: %v", err)
} else {
configEntry = osRelease["GRUB_ENTRY_NAME"]
// If its not empty override the defaultEntry and set the one set on the os-release file
if configEntry != "" {
defaultEntry = configEntry
}
}
if defaultEntry == "" {
g.logger.Warn("No default entry name for grub, not setting a name")
return nil
}
g.logger.Infof("Setting default grub entry to %s", defaultEntry)
return g.SetPersistentVariables(
filepath.Join(partMountPoint, constants.GrubOEMEnv),
map[string]string{"default_menu_entry": defaultEntry},
)
}
// Install installs grub into the device, copy the config file and add any extra TTY to grub
func (g *Grub) Install(rootDir, bootDir string) (err error) {
err = g.InstallEFI(rootDir, bootDir)
if err != nil {
return err
}
if !g.disableBootEntry {
image := g.grubEfiImg
if g.secureBoot {
image = g.shimImg
}
err = g.DoEFIEntries(filepath.Base(image), constants.BootDir)
if err != nil {
return err
}
}
return g.InstallConfig(rootDir, bootDir)
}
// InstallConfig installs grub configuraton files to the expected location.
// rootDir is the root of the OS image, bootDir is the folder grub read the
// configuration from, usually EFI partition mountpoint
func (g Grub) InstallConfig(rootDir, bootDir string) error {
for _, path := range g.grubPrefixes {
grubFile := filepath.Join(rootDir, g.elementalCfg)
if exists, _ := utils.Exists(g.fs, grubFile); !exists {
grubFile = filepath.Join(rootDir, g.legacyElementalCfg)
g.logger.Warnf("Grub config not found, using legacy config: %s", grubFile)
}
dstGrubFile := filepath.Join(bootDir, path, g.configFile)
g.logger.Infof("Using grub config file %s", grubFile)
// Create Needed dir under state partition to store the grub.cfg and any needed modules
err := utils.MkdirAll(g.fs, filepath.Join(bootDir, path), constants.DirPerm)
if err != nil {
return fmt.Errorf("error creating grub dir: %s", err)
}
g.logger.Infof("Copying grub config file from %s to %s", grubFile, dstGrubFile)
err = utils.CopyFile(g.fs, grubFile, dstGrubFile)
if err != nil {
g.logger.Errorf("Failed copying grub config file: %s", err)
return err
}
}
return nil
}