Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boot assesment for install and bootentry #604

Merged
merged 7 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions pkg/action/bootentries.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,16 @@ func selectBootEntrySystemd(cfg *config.Config, entry string) error {
}
}
}
bootName, err := bootNameToSystemdConf(entry)
bootFileName, err := bootNameToSystemdConf(entry)
if err != nil {
return err
}
assessment, err := utils.ReadAssessmentFromEntry(cfg.Fs, bootFileName, cfg.Logger)
if err != nil {
cfg.Logger.Logger.Err(err).Str("entry", entry).Str("boot file name", bootFileName).Msg("could not read assessment from entry")
return err
}
bootName := fmt.Sprintf("%s%s.conf", bootFileName, assessment)
// Set the default entry to the selected entry
systemdConf["default"] = bootName
err = utils.SystemdBootConfWriter(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/loader.conf"), systemdConf)
Expand Down Expand Up @@ -173,6 +179,10 @@ func systemdConfToBootName(conf string) (string, error) {

fileName := strings.TrimSuffix(conf, ".conf")

// Remove the boot assesment from the name we show
re := regexp.MustCompile(`\+\d+(-\d+)?$`)
fileName = re.ReplaceAllString(fileName, "")

if strings.HasPrefix(fileName, "active") {
bootName := "cos"
confName := strings.TrimPrefix(fileName, "active")
Expand Down Expand Up @@ -220,45 +230,47 @@ func systemdConfToBootName(conf string) (string, error) {
return strings.ReplaceAll(fileName, "_", " "), nil
}

// bootNameToSystemdConf converts a boot name to a systemd-boot conf file name
// skips the .conf extension
func bootNameToSystemdConf(name string) (string, error) {
differenciator := ""

if strings.HasPrefix(name, "cos") {
if name != "cos" {
differenciator = "_" + strings.TrimPrefix(name, "cos ")
}
return "active" + differenciator + ".conf", nil
return "active" + differenciator, nil
}

if strings.HasPrefix(name, "active") {
if name != "active" {
differenciator = "_" + strings.TrimPrefix(name, "active ")
}
return "active" + differenciator + ".conf", nil
return "active" + differenciator, nil
}

if strings.HasPrefix(name, "fallback") {
if name != "fallback" {
differenciator = "_" + strings.TrimPrefix(name, "fallback ")
}
return "passive" + differenciator + ".conf", nil
return "passive" + differenciator, nil
}

if strings.HasPrefix(name, "recovery") {
if name != "recovery" {
differenciator = "_" + strings.TrimPrefix(name, "recovery ")
}
return "recovery" + differenciator + ".conf", nil
return "recovery" + differenciator, nil
}

if strings.HasPrefix(name, "statereset") {
if name != "statereset" {
differenciator = "_" + strings.TrimPrefix(name, "statereset ")
}
return "statereset" + differenciator + ".conf", nil
return "statereset" + differenciator, nil
}

return strings.ReplaceAll(name, " ", "_") + ".conf", nil
return strings.ReplaceAll(name, " ", "_"), nil
}

// listBootEntriesSystemd lists the boot entries available in the systemd-boot config files
Expand Down
60 changes: 44 additions & 16 deletions pkg/action/bootentries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,42 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("does not exist"))
})
It("selects the boot entry in a default installation", func() {
It("works without boot assessment", func() {
err := fs.WriteFile("/efi/loader/entries/active.conf", []byte("title kairos\nefi /EFI/kairos/active.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/passive.conf", []byte("title kairos (fallback)\nefi /EFI/kairos/passive.efi\n"), os.ModePerm)
err = fs.WriteFile("/efi/loader/loader.conf", []byte("default active.conf"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/recovery.conf", []byte("title kairos recovery\nefi /EFI/kairos/recovery.efi\n"), os.ModePerm)

err = SelectBootEntry(config, "active")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/statereset.conf", []byte("title kairos state reset (auto)\nefi /EFI/kairos/statereset.efi\n"), os.ModePerm)
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to active"))
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("active.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
"/efi",
"",
syscall.MS_REMOUNT,
"")).To(BeTrue())
// Should have called a remount to make it RO
Expect(syscallMock.WasMountCalledWith(
"",
"/efi",
"",
syscall.MS_REMOUNT|syscall.MS_RDONLY,
"")).To(BeTrue())
})

It("selects the boot entry in a default installation", func() {
err := fs.WriteFile("/efi/loader/entries/active+2-1.conf", []byte("title kairos\nefi /EFI/kairos/active.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/passive+3.conf", []byte("title kairos (fallback)\nefi /EFI/kairos/passive.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/recovery+1-2.conf", []byte("title kairos recovery\nefi /EFI/kairos/recovery.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/statereset+2-1.conf", []byte("title kairos state reset (auto)\nefi /EFI/kairos/statereset.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/loader.conf", []byte(""), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
Expand All @@ -155,7 +183,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to fallback"))
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("passive.conf"))
Expect(reader["default"]).To(Equal("passive+3.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand All @@ -176,7 +204,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to recovery"))
reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("recovery.conf"))
Expect(reader["default"]).To(Equal("recovery+1-2.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand All @@ -197,7 +225,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to statereset"))
reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("statereset.conf"))
Expect(reader["default"]).To(Equal("statereset+2-1.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand All @@ -218,7 +246,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to cos"))
reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("active.conf"))
Expect(reader["default"]).To(Equal("active+2-1.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand All @@ -240,7 +268,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to active"))
reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("active.conf"))
Expect(reader["default"]).To(Equal("active+2-1.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand All @@ -260,7 +288,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
It("selects the boot entry in a extend-cmdline installation with boot branding", func() {
err := fs.WriteFile("/efi/loader/entries/active_install-mode_awesomeos.conf", []byte("title awesomeos\nefi /EFI/kairos/active_install-mode_awesomeos.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/passive_install-mode_awesomeos.conf", []byte("title awesomeos (fallback)\nefi /EFI/kairos/passive_install-mode_awesomeos.efi\n"), os.ModePerm)
err = fs.WriteFile("/efi/loader/entries/passive_install-mode_awesomeos+3.conf", []byte("title awesomeos (fallback)\nefi /EFI/kairos/passive_install-mode_awesomeos.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/recovery_install-mode_awesomeos.conf", []byte("title awesomeos recovery\nefi /EFI/kairos/recovery_install-mode_awesomeos.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
Expand All @@ -274,7 +302,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to fallback"))
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("passive_install-mode_awesomeos.conf"))
Expect(reader["default"]).To(Equal("passive_install-mode_awesomeos+3.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand Down Expand Up @@ -381,27 +409,27 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/active_foobar.conf", []byte("title Kairos\nefi /EFI/kairos/active_foobar.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/passive.conf", []byte("title Kairos (fallback)\nefi /EFI/kairos/passive.efi\n"), os.ModePerm)
err = fs.WriteFile("/efi/loader/entries/passive+3.conf", []byte("title Kairos (fallback)\nefi /EFI/kairos/passive.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/passive_foobar.conf", []byte("title Kairos (fallback)\nefi /EFI/kairos/passive_foobar.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/recovery.conf", []byte("title Kairos recovery\nefi /EFI/kairos/recovery.efi\n"), os.ModePerm)
err = fs.WriteFile("/efi/loader/entries/recovery+3.conf", []byte("title Kairos recovery\nefi /EFI/kairos/recovery.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/recovery_foobar.conf", []byte("title Kairos recovery\nefi /EFI/kairos/recovery_foobar.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/statereset.conf", []byte("title Kairos state reset (auto)\nefi /EFI/kairos/statereset.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/entries/statereset_foobar.conf", []byte("title Kairos state reset (auto)\nefi /EFI/kairos/state_reset_foobar.efi\n"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efi/loader/loader.conf", []byte(""), os.ModePerm)
err = fs.WriteFile("/efi/loader/loader.conf", []byte("default active.conf"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())

err = SelectBootEntry(config, "fallback")
Expect(err).ToNot(HaveOccurred())
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to fallback"))
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("passive.conf"))
Expect(reader["default"]).To(Equal("passive+3.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand Down Expand Up @@ -443,7 +471,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() {
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to recovery"))
reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
Expect(err).ToNot(HaveOccurred())
Expect(reader["default"]).To(Equal("recovery.conf"))
Expect(reader["default"]).To(Equal("recovery+3.conf"))
// Should have called a remount to make it RW
Expect(syscallMock.WasMountCalledWith(
"",
Expand Down
4 changes: 3 additions & 1 deletion pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ const (
StateResetBootSuffix = " state reset (auto)"

// Error
UpgradeNoSourceError = "Could not find a proper source for the upgrade.\nThis can be configured in the cloud config files under the 'upgrade.system.uri' key or via cmdline using the '--source' flag."
UpgradeNoSourceError = "Could not find a proper source for the upgrade.\nThis can be configured in the cloud config files under the 'upgrade.system.uri' key or via cmdline using the '--source' flag."
MultipleEntriesAssessmentError = "multiple boot entries found for %s"
NoBootAssessmentWarning = "No boot assessment found in current boot entry config file"
)

func UkiDefaultMenuEntries() []string {
Expand Down
6 changes: 6 additions & 0 deletions pkg/uki/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ func (i *InstallAction) Run() (err error) {
return fmt.Errorf("removing artifact set with role %s: %w", UnassignedArtifactRole, err)
}

// Add boot assessment to files by appending +3 to the name
err = utils.AddBootAssessment(i.cfg.Fs, i.spec.Partitions.EFI.MountPoint, i.cfg.Logger)
if err != nil {
i.cfg.Logger.Warnf("adding boot assesment: %s", err.Error())
}

// SelectBootEntry sets the default boot entry to the selected entry
err = action.SelectBootEntry(i.cfg, "cos")
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions pkg/uki/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ func (r *ResetAction) Run() (err error) {
r.cfg.Logger.Errorf("copying recovery to active: %s", err.Error())
return fmt.Errorf("copying recovery to active: %w", err)
}

// Add boot assessment to files by appending +3 to the name
err = elementalUtils.AddBootAssessment(r.cfg.Fs, r.spec.Partitions.EFI.MountPoint, r.cfg.Logger)
if err != nil {
r.cfg.Logger.Warnf("adding boot assesment: %s", err.Error())
}

// SelectBootEntry sets the default boot entry to the selected entry
err = action.SelectBootEntry(r.cfg, "cos")
// Should we fail? Or warn?
Expand Down
6 changes: 5 additions & 1 deletion pkg/uki/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ func (i *UpgradeAction) Run() (err error) {
i.cfg.Logger.Errorf("removing artifact set: %s", err.Error())
return fmt.Errorf("removing artifact set: %w", err)
}

// Add boot assessment to files by appending +3 to the name
err = elementalUtils.AddBootAssessment(i.cfg.Fs, i.spec.EfiPartition.MountPoint, i.cfg.Logger)
if err != nil {
i.cfg.Logger.Warnf("adding boot assesment: %s", err.Error())
}
// SelectBootEntry sets the default boot entry to the selected entry
err = action.SelectBootEntry(i.cfg, "cos")
// Should we fail? Or warn?
Expand Down
70 changes: 70 additions & 0 deletions pkg/utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -614,3 +615,72 @@ func CheckFailedInstallation(stateFile string) (bool, error) {
}
return false, nil
}

// AddBootAssessment adds boot assessment to files by appending +3 to the name
// Only for files that dont have it already as those are the ones upgraded
// Existing files that have a boot assessment will be left as is
// This should be called during install, upgrade and reset
// Mainly everything that updates the config files to point to a new artifact we need to reset the boot assessment
// as its a new artifact that needs to be assessed
func AddBootAssessment(fs v1.FS, artifactDir string, logger sdkTypes.KairosLogger) error {
return fsutils.WalkDirFs(fs, artifactDir, func(path string, info os.DirEntry, err error) error {
if err != nil {
return err
}
// Only do files that are conf files but dont match the loader.conf
if !info.IsDir() && filepath.Ext(path) == ".conf" && !strings.Contains(info.Name(), "loader.conf") {
dir := filepath.Dir(path)
ext := filepath.Ext(path)
base := strings.TrimSuffix(filepath.Base(path), ext)
// Lets check if the file has a boot assessment already. If it does, we dont need to do anything
// If it matches continue
re := regexp.MustCompile(`\+\d+(-\d+)?$`)
if re.MatchString(base) {
logger.Logger.Debug().Str("file", path).Msg("Boot assessment already present in file")
return nil
}
newBase := fmt.Sprintf("%s+3%s", base, ext)
newPath := filepath.Join(dir, newBase)
logger.Logger.Debug().Str("from", path).Str("to", newPath).Msg("Enabling boot assessment")
err = fs.Rename(path, newPath)
if err != nil {
logger.Logger.Err(err).Str("from", path).Str("to", newPath).Msg("Error renaming file")
return err
}
}

return nil
})
}

func ReadAssessmentFromEntry(fs v1.FS, entry string, logger sdkTypes.KairosLogger) (string, error) {
// Read current config for boot assessment from current config. We should already have the final config name
// Fix fallback and cos pointing to passive and active
if strings.HasPrefix(entry, "fallback") {
entry = strings.Replace(entry, "fallback", "passive", 1)
}
if strings.HasPrefix(entry, "cos") {
entry = strings.Replace(entry, "cos", "active", 1)
}
efiPart, err := partitions.GetEfiPartition(&logger)
if err != nil {
return "", err
}
// We only want the ones that match the assessment
currentfile, err := fsutils.GlobFs(fs, filepath.Join(efiPart.MountPoint, "loader/entries", entry+"+*.conf"))
if err != nil {
return "", err
}
if len(currentfile) == 0 {
return "", nil
}
if len(currentfile) > 1 {
return "", fmt.Errorf(cnst.MultipleEntriesAssessmentError, entry)
}
re := regexp.MustCompile(`(\+\d+(-\d+)?)\.conf$`)
if !re.MatchString(currentfile[0]) {
logger.Logger.Debug().Str("file", currentfile[0]).Msg(cnst.NoBootAssessmentWarning)
return "", nil
}
return re.FindStringSubmatch(currentfile[0])[1], nil
}
Loading
Loading