Skip to content

Commit

Permalink
Add boot assesment for install and bootentry (#604)
Browse files Browse the repository at this point in the history
  • Loading branch information
Itxaka authored Nov 27, 2024
1 parent 8516a19 commit 7be897c
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 25 deletions.
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

0 comments on commit 7be897c

Please sign in to comment.