Use grub binaries and libs from rootfs (#760)

This commit is contained in:
Itxaka 2025-04-25 10:43:21 +02:00 committed by GitHub
parent 5d5a52930f
commit d0f0710c78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 220 additions and 45 deletions

View File

@ -43,7 +43,11 @@ func (k Finish) Run(c config.Config, spec v1.Spec) error {
c.Logger.Logger.Info().Msg("Finished encrypt hook")
}
// Now that we have everything encrypted and ready if needed
// Now that we have everything encrypted and ready to mount if needed
err = GrubPostInstallOptions{}.Run(c, spec)
if err != nil {
return err
}
err = BundlePostInstall{}.Run(c, spec)
if err != nil {
c.Logger.Logger.Warn().Err(err).Msg("could not copy run bundles post install")

View File

@ -1,11 +1,13 @@
package hook
import (
"github.com/kairos-io/kairos-agent/v2/pkg/config"
cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"strings"
config "github.com/kairos-io/kairos-agent/v2/pkg/config"
"github.com/kairos-io/kairos-sdk/system"
"github.com/kairos-io/kairos-agent/v2/pkg/utils"
"github.com/kairos-io/kairos-sdk/machine"
"github.com/kairos-io/kairos-sdk/state"
"path/filepath"
)
type GrubOptions struct{}
@ -16,9 +18,9 @@ func (b GrubOptions) Run(c config.Config, _ v1.Spec) error {
}
c.Logger.Logger.Debug().Msg("Running GrubOptions hook")
c.Logger.Debugf("Setting grub options: %s", c.Install.GrubOptions)
err := system.Apply(system.SetGRUBOptions(c.Install.GrubOptions))
if err != nil && !strings.Contains(err.Error(), "0 errors occurred") {
c.Logger.Logger.Error().Err(err).Msg("Failed to set grub options")
err := grubOptions(c, c.Install.GrubOptions)
if err != nil {
return err
}
c.Logger.Logger.Debug().Msg("Finish GrubOptions hook")
return nil
@ -31,10 +33,32 @@ func (b GrubPostInstallOptions) Run(c config.Config, _ v1.Spec) error {
return nil
}
c.Logger.Logger.Debug().Msg("Running GrubOptions hook")
err := system.Apply(system.SetGRUBOptions(c.GrubOptions))
c.Logger.Debugf("Setting grub options: %s", c.GrubOptions)
err := grubOptions(c, c.GrubOptions)
if err != nil {
return err
}
c.Logger.Logger.Debug().Msg("Finish GrubOptions hook")
return nil
}
// grubOptions sets the grub options in the grubenv file
// It mounts the OEM partition if not already mounted
// If its mounted but RO, it remounts it as RW
func grubOptions(c config.Config, opts map[string]string) error {
runtime, err := state.NewRuntime()
if err != nil {
return err
}
if !runtime.OEM.Mounted {
err = machine.Mount(cnst.OEMLabel, cnst.OEMPath)
defer func() {
_ = machine.Umount(cnst.OEMPath)
}()
}
err = utils.SetPersistentVariables(filepath.Join(runtime.OEM.MountPoint, "grubenv"), opts, &c)
if err != nil {
c.Logger.Logger.Error().Err(err).Msg("Failed to set grub options")
}
c.Logger.Logger.Debug().Msg("Running GrubOptions hook")
return nil
return err
}

View File

@ -2,7 +2,7 @@ package hook
import (
"fmt"
config "github.com/kairos-io/kairos-agent/v2/pkg/config"
"github.com/kairos-io/kairos-agent/v2/pkg/config"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/kairos-io/kairos-sdk/utils"
"strings"
@ -15,8 +15,7 @@ type Interface interface {
// FinishInstall is a list of hooks that run when the install process is finished completely.
// Its mean for options that are not related to the install process itself
var FinishInstall = []Interface{
&GrubOptions{}, // Set custom GRUB options in OEM partition
&Lifecycle{}, // Handles poweroff/reboot by config options
&Lifecycle{}, // Handles poweroff/reboot by config options
}
// FinishReset is a list of hooks that run when the reset process is finished completely.
@ -46,7 +45,7 @@ var FinishUKIInstall = []Interface{
// PostInstall is a list of hooks that run after the install process has run.
// Runs things that need to be done before we run other post install stages like
// encrypting partitions, copying the install logs or installing bundles
// Most of this options are optional so they are not run by default unless specified int he config
// Most of this options are optional so they are not run by default unless specified in the config
var PostInstall = []Interface{
&Finish{},
}

View File

@ -60,7 +60,7 @@ func selectBootEntryGrub(cfg *config.Config, entry string) error {
vars := map[string]string{
"next_entry": entry,
}
err = utils.SetPersistentVariables("/oem/grubenv", vars, cfg.Fs)
err = utils.SetPersistentVariables("/oem/grubenv", vars, cfg)
if err != nil {
cfg.Logger.Errorf("could not set default boot entry: %s\n", err)
return err

View File

@ -694,7 +694,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
err = SelectBootEntry(config, "kairos")
Expect(err).ToNot(HaveOccurred())
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to kairos"))
variables, err := utils.ReadPersistentVariables("/oem/grubenv", fs)
variables, err := utils.ReadPersistentVariables("/oem/grubenv", config)
Expect(err).ToNot(HaveOccurred())
Expect(variables["next_entry"]).To(Equal("kairos"))
})

View File

@ -28,7 +28,6 @@ import (
agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/kairos-io/kairos-agent/v2/pkg/utils"
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
"github.com/kairos-io/kairos-sdk/collector"
@ -150,6 +149,16 @@ var _ = Describe("Install action tests", func() {
Expect(err).To(BeNil())
_, err = fs.Create(grubCfg)
Expect(err).To(BeNil())
// Create fake grub dir in rootfs and fake grub binaries
err = fsutils.MkdirAll(fs, filepath.Join(spec.Active.MountPoint, "sbin"), constants.DirPerm)
Expect(err).To(BeNil())
f, err := fs.Create(filepath.Join(spec.Active.MountPoint, "sbin", "grub2-install"))
Expect(err).To(BeNil())
Expect(f.Chmod(0755)).ToNot(HaveOccurred())
err = fsutils.MkdirAll(fs, filepath.Join(spec.Active.MountPoint, "usr", "lib", "grub", "i386-pc"), constants.DirPerm)
Expect(err).To(BeNil())
_, err = fs.Create(filepath.Join(spec.Active.MountPoint, "usr", "lib", "grub", "i386-pc", "modinfo.sh"))
Expect(err).To(BeNil())
mainDisk := sdkTypes.Disk{
Name: "device",
@ -349,9 +358,10 @@ var _ = Describe("Install action tests", func() {
Expect(cl.WasGetCalledWith("http://my.config.org")).To(BeTrue())
})
It("Fails on grub2-install errors", Label("grub"), func() {
It("Fails to find grub2-install", Label("grub"), func() {
spec.Target = device
cmdFail = utils.FindCommand("grub2-install", []string{"grub2-install", "grub-install"})
err := config.Fs.Remove(filepath.Join(spec.Active.MountPoint, "sbin", "grub2-install"))
Expect(err).To(BeNil())
Expect(installer.Run()).NotTo(BeNil())
Expect(runner.MatchMilestones([][]string{{"grub2-install"}}))
})
@ -362,5 +372,12 @@ var _ = Describe("Install action tests", func() {
Expect(installer.Run()).NotTo(BeNil())
Expect(runner.MatchMilestones([][]string{{"tune2fs", "-L", constants.PassiveLabel}}))
})
It("Fails if there is no grub2 artifacts", Label("grub"), func() {
spec.Target = device
err := config.Fs.Remove(filepath.Join(spec.Active.MountPoint, "usr", "lib", "grub", "i386-pc", "modinfo.sh"))
Expect(err).To(BeNil())
Expect(installer.Run()).NotTo(BeNil())
Expect(runner.MatchMilestones([][]string{{"grub2-install"}}))
})
})
})

View File

@ -130,7 +130,8 @@ var _ = Describe("Reset action tests", func() {
ghwTest.AddDisk(mainDisk)
ghwTest.CreateDevices()
fs.Create(constants.EfiDevice)
Expect(fsutils.MkdirAll(fs, constants.EfiDevice, constants.DirPerm)).ToNot(HaveOccurred())
bootedFrom = constants.SystemLabel
runner.SideEffect = func(cmd string, args ...string) ([]byte, error) {
if cmd == cmdFail {

View File

@ -584,7 +584,7 @@ func (e Elemental) SetDefaultGrubEntry(partMountPoint string, imgMountPoint stri
return utils.SetPersistentVariables(
filepath.Join(partMountPoint, cnst.GrubOEMEnv),
map[string]string{"default_menu_entry": defaultEntry},
e.config.Fs,
e.config,
)
}

View File

@ -848,7 +848,7 @@ var _ = Describe("Elemental", Label("elemental"), func() {
el := elemental.NewElemental(config)
Expect(config.Fs.Mkdir("/tmp", cnst.DirPerm)).To(BeNil())
Expect(el.SetDefaultGrubEntry("/tmp", "/imgMountpoint", "dio")).To(BeNil())
varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config.Fs)
varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config)
Expect(err).To(BeNil())
Expect(varsParsed["default_menu_entry"]).To(Equal("dio"))
})
@ -856,7 +856,7 @@ var _ = Describe("Elemental", Label("elemental"), func() {
el := elemental.NewElemental(config)
Expect(config.Fs.Mkdir("/mountpoint", cnst.DirPerm)).To(BeNil())
Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountPoint", "")).To(BeNil())
_, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config.Fs)
_, err := utils.ReadPersistentVariables(filepath.Join("/tmp", cnst.GrubOEMEnv), config)
// Because it didnt do anything due to the entry being empty, the file should not be there
Expect(err).ToNot(BeNil())
_, err = config.Fs.Stat(filepath.Join("/tmp", cnst.GrubOEMEnv))
@ -871,7 +871,7 @@ var _ = Describe("Elemental", Label("elemental"), func() {
el := elemental.NewElemental(config)
Expect(el.SetDefaultGrubEntry("/mountpoint", "/imgMountPoint", "")).To(BeNil())
varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/mountpoint", cnst.GrubOEMEnv), config.Fs)
varsParsed, err := utils.ReadPersistentVariables(filepath.Join("/mountpoint", cnst.GrubOEMEnv), config)
Expect(err).To(BeNil())
Expect(varsParsed["default_menu_entry"]).To(Equal("test"))

View File

@ -503,11 +503,27 @@ func CalcFileChecksum(fs v1.FS, fileName string) (string, error) {
// FindCommand will search for the command(s) in the options given to find the current command
// If it cant find it returns the default value give. Useful for the same binaries with different names across OS
func FindCommand(defaultPath string, options []string) string {
func FindCommand(fs v1.FS, defaultPath string, options []string) string {
for _, p := range options {
path, err := exec.LookPath(p)
if err == nil {
return path
// If its a full path, check if it exists directly
if strings.Contains(p, "/") {
d, err := fs.Stat(p)
if err != nil {
continue
}
if d.IsDir() {
continue
}
m := d.Mode()
// Check if its executable
if m&0111 != 0 {
return p
}
} else {
path, err := exec.LookPath(p)
if err == nil {
return path
}
}
}

View File

@ -24,6 +24,7 @@ import (
"github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
"github.com/kairos-io/kairos-sdk/utils"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
@ -61,16 +62,38 @@ func (g Grub) Install(target, rootDir, bootDir, grubConf, tty string, efi bool,
if !efi {
g.config.Logger.Info("Installing GRUB..")
// Find where in the rootDir the grub2 files for i386-pc are
// Use the modinfo.sh as a marker
grubdir = findGrubDir(g.config.Fs, rootDir)
if grubdir == "" {
g.config.Logger.Logger.Error().Str("path", rootDir).Msg("Failed to find grub dir")
return fmt.Errorf("failed to find grub dir under %s", rootDir)
}
grubargs = append(
grubargs,
fmt.Sprintf("--root-directory=%s", rootDir),
fmt.Sprintf("--directory=%s", grubdir),
fmt.Sprintf("--boot-directory=%s", bootDir),
"--target=i386-pc",
target,
)
g.config.Logger.Debugf("Running grub with the following args: %s", grubargs)
out, err := g.config.Runner.Run(FindCommand("grub2-install", []string{"grub2-install", "grub-install"}), grubargs...)
grubBin := FindCommand(g.config.Fs, "", []string{
filepath.Join(rootDir, "/usr/sbin/", "grub2-install"),
filepath.Join(rootDir, "/usr/bin/", "grub2-install"),
filepath.Join(rootDir, "/sbin/", "grub2-install"),
filepath.Join(rootDir, "/usr/sbin/", "grub-install"),
filepath.Join(rootDir, "/usr/bin/", "grub-install"),
filepath.Join(rootDir, "/sbin/", "grub-install"),
})
g.config.Logger.Logger.Debug().Str("command", grubBin).Msg("Found grub binary")
if grubBin == "" {
g.config.Logger.Logger.Error().Str("path", rootDir).Msg("Grub binary not found in path")
return fmt.Errorf("grub binary not found in path")
}
out, err := g.config.Runner.Run(grubBin, grubargs...)
if err != nil {
g.config.Logger.Errorf(string(out))
return err
@ -257,20 +280,62 @@ func (g Grub) Install(target, rootDir, bootDir, grubConf, tty string, efi bool,
return nil
}
// SetPersistentVariables sets the given vars into the given grubEnvFile for grub to read them
func SetPersistentVariables(grubEnvFile string, vars map[string]string, fs v1.FS) error {
// findGrubDir will find the grub dir under the dir given if possible by searching for the modinfo.sh
// And it will return the full dir path where the modinfo.sh is contained
func findGrubDir(vfs v1.FS, dir string) string {
var foundPath string
_ = fsutils.WalkDirFs(vfs, dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Name() == "modinfo.sh" && strings.Contains(path, "i386-pc") {
// We found the grub dir, return it
foundPath = filepath.Dir(path)
return nil
}
return nil
})
return foundPath
}
func SetPersistentVariables(grubEnvFile string, vars map[string]string, c *agentConfig.Config) error {
var b bytes.Buffer
// Write header
b.WriteString("# GRUB Environment Block\n")
keys := make([]string, 0, len(vars))
for k := range vars {
// First we need to read the existing values from the grubenv file if they exist
finalVars, err := ReadPersistentVariables(grubEnvFile, c)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error reading existing grubenv file %s: %s", grubEnvFile, err)
}
}
// Check if we have a nil var
if len(finalVars) != 0 {
c.Logger.Logger.Debug().Interface("existingVars", finalVars).Msg("Existing grubenv variables")
}
// Merge the existing vars with the new ones
// existing vars will be overridden by the new ones from vars if they match
for key, newValue := range vars {
if oldValue, exists := finalVars[key]; exists {
c.Logger.Logger.Warn().Str("key", key).Str("oldValue", oldValue).Str("newValue", newValue).Msg("Overriding existing grubenv variable")
}
finalVars[key] = newValue
}
keys := make([]string, 0, len(finalVars))
for k := range finalVars {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if len(vars[k]) > 0 {
b.WriteString(fmt.Sprintf("%s=%s\n", k, vars[k]))
if len(finalVars[k]) > 0 {
b.WriteString(fmt.Sprintf("%s=%s\n", k, finalVars[k]))
}
}
@ -280,7 +345,7 @@ func SetPersistentVariables(grubEnvFile string, vars map[string]string, fs v1.FS
for i := 0; i < toBeFilled; i++ {
b.WriteByte('#')
}
return fs.WriteFile(grubEnvFile, b.Bytes(), cnst.FilePerm)
return c.Fs.WriteFile(grubEnvFile, b.Bytes(), cnst.FilePerm)
}
// copyGrubFonts will try to finds and copy the needed grub fonts into the system
@ -397,11 +462,12 @@ func (g Grub) copyGrub() error {
}
// ReadPersistentVariables will read a grub env file and parse the values
func ReadPersistentVariables(grubEnvFile string, fs v1.FS) (map[string]string, error) {
func ReadPersistentVariables(grubEnvFile string, c *agentConfig.Config) (map[string]string, error) {
vars := make(map[string]string)
f, err := fs.ReadFile(grubEnvFile)
f, err := c.Fs.ReadFile(grubEnvFile)
if err != nil {
return nil, err
return vars, err
}
for _, a := range strings.Split(string(f), "\n") {
// comment or fillup, so skip
@ -412,7 +478,7 @@ func ReadPersistentVariables(grubEnvFile string, fs v1.FS) (map[string]string, e
if len(splitted) == 2 {
vars[splitted[0]] = splitted[1]
} else {
return nil, fmt.Errorf("invalid format for %s", a)
return vars, fmt.Errorf("invalid format for %s", a)
}
}
return vars, nil

View File

@ -691,6 +691,17 @@ var _ = Describe("Utils", Label("utils"), func() {
err = fs.WriteFile(filepath.Join(rootDir, constants.GrubConf), []byte("console=tty1"), 0644)
Expect(err).ShouldNot(HaveOccurred())
// Create fake grub dir in rootfs and fake grub binaries
err = fsutils.MkdirAll(fs, filepath.Join(rootDir, "sbin"), constants.DirPerm)
Expect(err).To(BeNil())
f, err := fs.Create(filepath.Join(rootDir, "sbin", "grub2-install"))
Expect(err).To(BeNil())
Expect(f.Chmod(0755)).ToNot(HaveOccurred())
err = fsutils.MkdirAll(fs, filepath.Join(rootDir, "usr", "lib", "grub", "i386-pc"), constants.DirPerm)
Expect(err).To(BeNil())
_, err = fs.Create(filepath.Join(rootDir, "usr", "lib", "grub", "i386-pc", "modinfo.sh"))
Expect(err).To(BeNil())
})
It("installs with default values", func() {
grub := utils.NewGrub(config)
@ -741,6 +752,18 @@ var _ = Describe("Utils", Label("utils"), func() {
Expect(err).To(BeNil())
})
It("fails with bios if no grub2-install file exists", func() {
Expect(fs.RemoveAll(filepath.Join(rootDir, "sbin", "grub2-install"))).ToNot(HaveOccurred())
grub := utils.NewGrub(config)
err := grub.Install(target, rootDir, bootDir, constants.GrubConf, "", false, "")
Expect(err).To(HaveOccurred())
})
It("fails with bios if no modules files exists", func() {
Expect(fs.RemoveAll(filepath.Join(rootDir, "usr", "lib", "grub", "i386-pc"))).ToNot(HaveOccurred())
grub := utils.NewGrub(config)
err := grub.Install(target, rootDir, bootDir, constants.GrubConf, "", false, "")
Expect(err).To(HaveOccurred())
})
It("fails with efi if no modules files exist", Label("efi"), func() {
grub := utils.NewGrub(config)
err := grub.Install(target, rootDir, bootDir, constants.GrubConf, "", true, "")
@ -804,9 +827,9 @@ var _ = Describe("Utils", Label("utils"), func() {
defer os.Remove(temp.Name())
Expect(utils.SetPersistentVariables(
temp.Name(), map[string]string{"key1": "value1", "key2": "value2"},
config.Fs,
config,
)).To(BeNil())
readVars, err := utils.ReadPersistentVariables(temp.Name(), config.Fs)
readVars, err := utils.ReadPersistentVariables(temp.Name(), config)
Expect(err).To(BeNil())
Expect(readVars["key1"]).To(Equal("value1"))
Expect(readVars["key2"]).To(Equal("value2"))
@ -814,10 +837,35 @@ var _ = Describe("Utils", Label("utils"), func() {
It("Fails setting variables", func() {
e := utils.SetPersistentVariables(
"badfilenopath", map[string]string{"key1": "value1"},
config.Fs,
config,
)
Expect(e).NotTo(BeNil())
})
It("respects existing variables", func() {
temp, err := os.CreateTemp("", "grub-*")
Expect(err).ShouldNot(HaveOccurred())
defer os.Remove(temp.Name())
Expect(utils.SetPersistentVariables(
temp.Name(), map[string]string{"key1": "value1", "key2": "value2"},
config,
)).To(BeNil())
readVars, err := utils.ReadPersistentVariables(temp.Name(), config)
Expect(err).To(BeNil())
Expect(readVars["key1"]).To(Equal("value1"))
Expect(readVars["key2"]).To(Equal("value2"))
// Now we do it again with a different value
Expect(utils.SetPersistentVariables(
temp.Name(), map[string]string{"key1": "value3", "key3": "value4"},
config,
)).To(BeNil())
// Now there should be an extra key and key1 should be updated
readVars, err = utils.ReadPersistentVariables(temp.Name(), config)
Expect(err).To(BeNil())
Expect(readVars["key1"]).To(Equal("value3"))
Expect(readVars["key2"]).To(Equal("value2"))
Expect(readVars["key3"]).To(Equal("value4"))
})
})
})
Describe("CreateSquashFS", Label("CreateSquashFS"), func() {