From 619ee5b295740aa5449da1f30e2c8ae6750b9ed5 Mon Sep 17 00:00:00 2001 From: Itxaka Date: Mon, 25 Nov 2024 17:05:11 +0100 Subject: [PATCH] Read and write the assesment part on the fly Signed-off-by: Itxaka --- pkg/action/bootentries.go | 26 +++++++++++---- pkg/action/bootentries_test.go | 60 +++++++++++++++++++++++++--------- pkg/utils/common.go | 35 ++++++++++++++++---- pkg/utils/fs/fs.go | 35 ++++++++++++++++++++ 4 files changed, 127 insertions(+), 29 deletions(-) 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/utils/common.go b/pkg/utils/common.go index 23ad089c..6091d488 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -649,11 +649,34 @@ func AddBootAssessment(fs v1.FS, artifactDir string, logger sdkTypes.KairosLogge }) } -func readAssesmentFromBootEntry(bootEntry string) (int, error) { - re := regexp.MustCompile(`\+(\d+)(-\d+)?$`) - match := re.FindStringSubmatch(bootEntry) - if len(match) == 0 { - return 0, fmt.Errorf("No match found") +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) } - return strconv.Atoi(match[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("multiple boot entries found for %s", entry) + } + re := regexp.MustCompile(`(\+\d+(-\d+)?)\.conf$`) + if !re.MatchString(currentfile[0]) { + logger.Logger.Debug().Str("file", currentfile[0]).Msg("No boot assessment found in current boot entry config file") + 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 +}