diff --git a/pkg/action/bootentries.go b/pkg/action/bootentries.go index c843352c..0a8bc240 100644 --- a/pkg/action/bootentries.go +++ b/pkg/action/bootentries.go @@ -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) @@ -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") @@ -220,6 +230,8 @@ 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 := "" @@ -227,38 +239,38 @@ func bootNameToSystemdConf(name string) (string, error) { 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 diff --git a/pkg/action/bootentries_test.go b/pkg/action/bootentries_test.go index c5f2bbb8..ca8a8db3 100644 --- a/pkg/action/bootentries_test.go +++ b/pkg/action/bootentries_test.go @@ -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()) @@ -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( "", @@ -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( "", @@ -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( "", @@ -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( "", @@ -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( "", @@ -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()) @@ -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( "", @@ -381,11 +409,11 @@ 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()) @@ -393,7 +421,7 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { 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") @@ -401,7 +429,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( "", @@ -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( "", diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 66b37624..0a3be469 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -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 { diff --git a/pkg/uki/install.go b/pkg/uki/install.go index 2198a3c9..07ac6649 100644 --- a/pkg/uki/install.go +++ b/pkg/uki/install.go @@ -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 { diff --git a/pkg/uki/reset.go b/pkg/uki/reset.go index 57d8d7db..244ef3dc 100644 --- a/pkg/uki/reset.go +++ b/pkg/uki/reset.go @@ -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? diff --git a/pkg/uki/upgrade.go b/pkg/uki/upgrade.go index 7b369ed2..9bc3f0d4 100644 --- a/pkg/uki/upgrade.go +++ b/pkg/uki/upgrade.go @@ -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? diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 919e0305..3a7ea151 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -28,6 +28,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -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 +} diff --git a/pkg/utils/fs/fs.go b/pkg/utils/fs/fs.go index 91bdd738..27448350 100644 --- a/pkg/utils/fs/fs.go +++ b/pkg/utils/fs/fs.go @@ -273,3 +273,38 @@ func Copy(fs v1.FS, src, dst string) error { } return nil } + +// GlobFs returns the names of all files matching pattern or nil if there is no matching file. +// Only consider the names of files in the directory included in the pattern, not in subdirectories. +// So the pattern "dir/*" will return only the files in the directory "dir", not in "dir/subdir". +func GlobFs(fs v1.FS, pattern string) ([]string, error) { + var matches []string + + // Check if the pattern is well formed. + if _, err := filepath.Match(pattern, ""); err != nil { + return nil, err + } + + // Split the pattern into directory and file parts. + dir, file := filepath.Split(pattern) + if dir == "" { + dir = "." + } + + // Read the directory. + entries, err := fs.ReadDir(dir) + if err != nil { + return nil, err + } + + // Match the entries against the pattern. + for _, entry := range entries { + if matched, err := filepath.Match(file, entry.Name()); err != nil { + return nil, err + } else if matched { + matches = append(matches, filepath.Join(dir, entry.Name())) + } + } + + return matches, nil +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 8a5eb0e1..ef130e7d 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -32,6 +32,7 @@ import ( "github.com/kairos-io/kairos-agent/v2/pkg/utils" "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" "github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" + "github.com/kairos-io/kairos-agent/v2/tests/matchers" v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks" ghwMock "github.com/kairos-io/kairos-sdk/ghw/mocks" sdkTypes "github.com/kairos-io/kairos-sdk/types" @@ -1062,4 +1063,206 @@ var _ = Describe("Utils", Label("utils"), func() { Expect(utils.IsUkiWithFs(fs)).To(BeFalse()) }) }) + Describe("AddBootAssessment", func() { + BeforeEach(func() { + Expect(fsutils.MkdirAll(fs, "/efi/loader/entries", os.ModePerm)).ToNot(HaveOccurred()) + }) + It("adds the boot assessment to a file", func() { + err := fs.WriteFile("/efi/loader/entries/test.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = utils.AddBootAssessment(fs, "/efi/loader/entries", logger) + Expect(err).ToNot(HaveOccurred()) + Expect("/efi/loader/entries/test.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + // Should match with the +3 + Expect("/efi/loader/entries/test+3.conf").To(matchers.BeAnExistingFileFs(fs)) + }) + It("adds the boot assessment to several files", func() { + err := fs.WriteFile("/efi/loader/entries/test1.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test3.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = utils.AddBootAssessment(fs, "/efi/loader/entries", logger) + Expect(err).ToNot(HaveOccurred()) + Expect("/efi/loader/entries/test1.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test2.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test3.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + // Should match with the +3 + Expect("/efi/loader/entries/test1+3.conf").To(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test2+3.conf").To(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test3+3.conf").To(matchers.BeAnExistingFileFs(fs)) + }) + It("leaves assessment in place for existing files", func() { + err := fs.WriteFile("/efi/loader/entries/test1.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test2+3.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test3+1-2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = utils.AddBootAssessment(fs, "/efi/loader/entries", logger) + Expect(err).ToNot(HaveOccurred()) + Expect("/efi/loader/entries/test1.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test3+3.conf").ToNot(matchers.BeAnExistingFileFs(fs)) + // Should match with the +3 and the existing ones left in place + Expect("/efi/loader/entries/test1+3.conf").To(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test2+3.conf").To(matchers.BeAnExistingFileFs(fs)) + Expect("/efi/loader/entries/test3+1-2.conf").To(matchers.BeAnExistingFileFs(fs)) + }) + It("fails to write the boot assessment in non existing dir", func() { + err := utils.AddBootAssessment(fs, "/fake", logger) + Expect(err).To(HaveOccurred()) + }) + }) + Describe("ReadAssessmentFromEntry", func() { + var ghwTest ghwMock.GhwMock + BeforeEach(func() { + Expect(fsutils.MkdirAll(fs, "/efi/loader/entries", os.ModePerm)).ToNot(HaveOccurred()) + mainDisk := sdkTypes.Disk{ + Name: "device", + Partitions: []*sdkTypes.Partition{ + { + Name: "device1", + FilesystemLabel: "COS_GRUB", + FS: "ext4", + MountPoint: "/efi", + }, + }, + } + ghwTest = ghwMock.GhwMock{} + ghwTest.AddDisk(mainDisk) + ghwTest.CreateDevices() + }) + AfterEach(func() { + ghwTest.Clean() + }) + It("reads the assessment from a file", func() { + err := fs.WriteFile("/efi/loader/entries/test+2-1.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + entry, err := utils.ReadAssessmentFromEntry(fs, "test", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+2-1")) + }) + It("reads passive when using fallback", func() { + err := fs.WriteFile("/efi/loader/entries/passive+2-1.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + // fallback should point to passive + entry, err := utils.ReadAssessmentFromEntry(fs, "fallback", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+2-1")) + + // Should find the passive entry as well directly + entry, err = utils.ReadAssessmentFromEntry(fs, "passive", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+2-1")) + }) + It("reads active when using cos", func() { + err := fs.WriteFile("/efi/loader/entries/active+1-2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + // cos should point to active + entry, err := utils.ReadAssessmentFromEntry(fs, "cos", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+1-2")) + + // Should find the active entry as well directly + entry, err = utils.ReadAssessmentFromEntry(fs, "active", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+1-2")) + }) + + It("empty assessment if it doesnt match", func() { + entry, err := utils.ReadAssessmentFromEntry(fs, "cos", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + + // Should find the active entry as well directly + entry, err = utils.ReadAssessmentFromEntry(fs, "active", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + }) + + It("fails with no EFI partition", func() { + ghwTest.Clean() + entry, err := utils.ReadAssessmentFromEntry(fs, "cos", logger) + Expect(err).To(HaveOccurred()) + Expect(entry).To(Equal("")) + }) + + It("errors if more than one file matches", func() { + err := fs.WriteFile("/efi/loader/entries/active+1-2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/active+3-2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + entry, err := utils.ReadAssessmentFromEntry(fs, "active", logger) + Expect(err).To(HaveOccurred()) + Expect(entry).To(Equal("")) + Expect(err.Error()).To(Equal(fmt.Sprintf(constants.MultipleEntriesAssessmentError, "active"))) + }) + + It("errors if dir doesn't exist", func() { + // Remove all dirs + cleanup() + entry, err := utils.ReadAssessmentFromEntry(fs, "active", logger) + Expect(err).To(HaveOccurred()) + Expect(entry).To(Equal("")) + // Check that error is os.ErrNotExist + Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue()) + }) + + It("matches with weird but valid format", func() { + // This are valid values, after all the asessment is just a string at the end that starts with + and has + // and number and an optional dash after that. It has to be before the .conf so this are valid values + // even if they are weird or stupid. + // potentially the name can be this if someone is rebuilding efi files and adding the + to indicate the build number + // for example. + // We dont use this but still want to check if these are valid. + err := fs.WriteFile("/efi/loader/entries/test1++++++5.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test2+3+3-1.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + entry, err := utils.ReadAssessmentFromEntry(fs, "test1", logger) + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+5")) + entry, err = utils.ReadAssessmentFromEntry(fs, "test2", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("+3-1")) + }) + It("doesn't match assessment if format is wrong", func() { + err := fs.WriteFile("/efi/loader/entries/test1+1djnfsdjknfsdajf2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test2+1-sadfsbauhdfkj.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test3+asdasd.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test4+-2.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + err = fs.WriteFile("/efi/loader/entries/test5+3&4.conf", []byte(""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + entry, err := utils.ReadAssessmentFromEntry(fs, "test1", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + entry, err = utils.ReadAssessmentFromEntry(fs, "test2", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + entry, err = utils.ReadAssessmentFromEntry(fs, "test3", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + entry, err = utils.ReadAssessmentFromEntry(fs, "test4", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + entry, err = utils.ReadAssessmentFromEntry(fs, "test5", logger) + // It actually does not error but just doesn't match + Expect(err).ToNot(HaveOccurred()) + Expect(entry).To(Equal("")) + }) + + }) }) diff --git a/tests/matchers/fs.go b/tests/matchers/fs.go new file mode 100644 index 00000000..8ba2fc7e --- /dev/null +++ b/tests/matchers/fs.go @@ -0,0 +1,44 @@ +package matchers + +import ( + "fmt" + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" + "github.com/twpayne/go-vfs/v5" + "os" +) + +// BeAnExistingFileFs returns a matcher that checks if a file exists in the given vfs. +func BeAnExistingFileFs(fs vfs.FS) types.GomegaMatcher { + return &beAnExistingFileFsMatcher{ + fs: fs, + } +} + +type beAnExistingFileFsMatcher struct { + fs vfs.FS +} + +func (matcher *beAnExistingFileFsMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("BeAnExistingFileFs matcher expects a file path") + } + // Here is the magic, check existence against a vfs + if _, err = matcher.fs.Stat(actualFilename); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + + return true, nil +} + +func (matcher *beAnExistingFileFsMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to exist") +} + +func (matcher *beAnExistingFileFsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to exist") +}