From 027a8800c2d0bcbccfebc13c35c8edee917c5d3c Mon Sep 17 00:00:00 2001 From: Mauro Morales Date: Fri, 15 Mar 2024 09:17:37 +0100 Subject: [PATCH] Use existing role names for bootentry command on UKI (#247) * Use existing role names for bootentry command Switch from active.conf, passive.conf and recovery.conf to cos, fallback and recovery respectively Signed-off-by: Mauro Morales * Extended cmdline Signed-off-by: Mauro Morales * Extend tests Signed-off-by: Mauro Morales --------- Signed-off-by: Mauro Morales --- pkg/action/bootentries.go | 114 +++++++++++++-- pkg/action/bootentries_test.go | 252 ++++++++++++++++++++++++++++++--- 2 files changed, 338 insertions(+), 28 deletions(-) diff --git a/pkg/action/bootentries.go b/pkg/action/bootentries.go index 3c2265f..3ae3f9d 100644 --- a/pkg/action/bootentries.go +++ b/pkg/action/bootentries.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "regexp" "strings" "syscall" @@ -65,11 +66,6 @@ func selectBootEntryGrub(cfg *config.Config, entry string) error { // selectBootEntrySystemd sets the default boot entry to the selected entry via modifying the loader.conf file // also validates that the entry exists in our list of entries func selectBootEntrySystemd(cfg *config.Config, entry string) error { - // Read the systemd-boot conf file - if !strings.HasSuffix(entry, ".conf") { - entry = entry + ".conf" - } - cfg.Logger.Infof("Setting default boot entry to %s", entry) // Get EFI partition @@ -83,6 +79,13 @@ func selectBootEntrySystemd(cfg *config.Config, entry string) error { return err } + originalEntries := entries + // when there are only 3 entries, we can assume they are active, passive and recovery + if len(entries) == 3 { + entries = []string{"cos", "fallback", "recovery"} + } + // when there are more than 3 entries, then we need to also extract the part between the first _ and the .conf in order to distinguish the entries + // Check that entry exists in the entries list err = entryInList(cfg, entry, entries) if err != nil { @@ -108,8 +111,19 @@ func selectBootEntrySystemd(cfg *config.Config, entry string) error { cfg.Logger.Errorf("could not read loader.conf: %s", err) return err } + if !reflect.DeepEqual(originalEntries, entries) { + for _, e := range originalEntries { + if strings.HasPrefix(e, entry) { + entry = e + } + } + } + bootName, err := bootNameToSystemdConf(entry) + if err != nil { + return err + } // Set the default entry to the selected entry - systemdConf["default"] = entry + systemdConf["default"] = bootName err = utils.SystemdBootConfWriter(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/loader.conf"), systemdConf) if err != nil { cfg.Logger.Errorf("could not write loader.conf: %s", err) @@ -141,6 +155,76 @@ func listBootEntriesGrub(cfg *config.Config) error { return err } +func systemdConfToBootName(conf string) (string, error) { + if !strings.HasSuffix(conf, ".conf") { + return "", fmt.Errorf("unknown systemd-boot conf: %s", conf) + } + + fileName := strings.TrimSuffix(conf, ".conf") + + if strings.HasPrefix(fileName, "active") { + bootName := "cos" + confName := strings.TrimPrefix(fileName, "active") + + if confName != "" { + bootName = bootName + " " + strings.Trim(confName, "_") + } + + return bootName, nil + } + + if strings.HasPrefix(fileName, "passive") { + bootName := "fallback" + confName := strings.TrimPrefix(fileName, "passive") + + if confName != "" { + bootName = bootName + " " + strings.Trim(confName, "_") + } + + return bootName, nil + } + + if strings.HasPrefix(conf, "recovery") { + bootName := "recovery" + confName := strings.TrimPrefix(fileName, "recovery") + + if confName != "" { + bootName = bootName + " " + strings.Trim(confName, "_") + } + + return bootName, nil + } + + return "", fmt.Errorf("unknown systemd-boot conf: %s", conf) +} + +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 + } + + if strings.HasPrefix(name, "fallback") { + if name != "fallback" { + differenciator = "_" + strings.TrimPrefix(name, "fallback ") + } + return "passive" + differenciator + ".conf", nil + } + + if strings.HasPrefix(name, "recovery") { + if name != "recovery" { + differenciator = "_" + strings.TrimPrefix(name, "recovery ") + } + return "recovery" + differenciator + ".conf", nil + } + + return "", fmt.Errorf("unknown boot name: %s", name) +} + // listBootEntriesSystemd lists the boot entries available in the systemd-boot config files // and prompts the user to select one // then calls the underlying SelectBootEntry function to mange the entry writing and validation @@ -157,15 +241,24 @@ func listBootEntriesSystemd(cfg *config.Config) error { } entries, err := listSystemdEntries(cfg, efiPartition) + if err != nil { + return err + } + + currentlySelected, err := systemdConfToBootName(loaderConf["default"]) // create a selector - selector := selection.New(fmt.Sprintf("Select Boot Entry (current entry: %s)", loaderConf["default"]), entries) + selector := selection.New(fmt.Sprintf("Select Boot Entry (current entry: %s)", currentlySelected), entries) selector.Filter = nil // Remove the filter selector.ResultTemplate = `` // Do not print the result as we are asking for confirmation afterwards selected, _ := selector.RunPrompt() c := confirmation.New("Are you sure you want to change the boot entry to "+selected, confirmation.Yes) c.ResultTemplate = `` confirm, err := c.RunPrompt() + if err != nil { + return err + } + if confirm { return SelectBootEntry(cfg, selected) } @@ -186,7 +279,12 @@ func listSystemdEntries(cfg *config.Config, efiPartition *v1.Partition) ([]strin if filepath.Ext(info.Name()) != ".conf" { return nil } - entries = append(entries, info.Name()) + entry, err := systemdConfToBootName(info.Name()) + if err != nil { + return err + } + + entries = append(entries, entry) return nil }) return entries, err diff --git a/pkg/action/bootentries_test.go b/pkg/action/bootentries_test.go index bc817dd..26143a8 100644 --- a/pkg/action/bootentries_test.go +++ b/pkg/action/bootentries_test.go @@ -2,6 +2,9 @@ package action import ( "bytes" + "os" + "syscall" + "github.com/jaypipes/ghw/pkg/block" agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" @@ -14,8 +17,6 @@ import ( . "github.com/onsi/gomega" "github.com/twpayne/go-vfs" "github.com/twpayne/go-vfs/vfst" - "os" - "syscall" ) var _ = Describe("Bootentries tests", Label("bootentry"), func() { @@ -107,17 +108,17 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { It("lists the boot entries if there is any", func() { err := fs.WriteFile("/efi/loader/loader.conf", []byte("timeout 5\ndefault kairos\nrecovery kairos2\n"), os.ModePerm) Expect(err).ToNot(HaveOccurred()) - err = fs.WriteFile("/efi/loader/entries/kairos.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) + err = fs.WriteFile("/efi/loader/entries/active.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) Expect(err).ToNot(HaveOccurred()) - err = fs.WriteFile("/efi/loader/entries/kairos2.conf", []byte("title kairos2\nlinux /vmlinuz2\ninitrd /initrd2\noptions root=LABEL=COS_GRUB2\n"), os.ModePerm) + err = fs.WriteFile("/efi/loader/entries/passive.conf", []byte("title kairos2\nlinux /vmlinuz2\ninitrd /initrd2\noptions root=LABEL=COS_GRUB2\n"), os.ModePerm) Expect(err).ToNot(HaveOccurred()) entries, err := listSystemdEntries(config, &v1.Partition{MountPoint: "/efi"}) Expect(err).ToNot(HaveOccurred()) Expect(entries).To(HaveLen(2)) - Expect(entries).To(ContainElement("kairos.conf")) - Expect(entries).To(ContainElement("kairos2.conf")) + Expect(entries).To(ContainElement("cos")) + Expect(entries).To(ContainElement("fallback")) }) It("list empty boot entries if there is none", func() { @@ -133,19 +134,43 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("does not exist")) }) - It("selects the boot entry", func() { - err := fs.WriteFile("/efi/loader/entries/kairos.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) + It("selects the boot entry in a default installation", 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/kairos2.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) + err = fs.WriteFile("/efi/loader/entries/passive.conf", []byte("title kairos (fallback)\nefi /EFI/kairos/passive.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) Expect(err).ToNot(HaveOccurred()) err = fs.WriteFile("/efi/loader/loader.conf", []byte(""), os.ModePerm) - - err = SelectBootEntry(config, "kairos.conf") Expect(err).ToNot(HaveOccurred()) - Expect(memLog.String()).To(ContainSubstring("Default boot entry set to kairos")) + + 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("kairos.conf")) + Expect(reader["default"]).To(Equal("passive.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()) + + err = SelectBootEntry(config, "recovery") + Expect(err).ToNot(HaveOccurred()) + 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")) // Should have called a remount to make it RW Expect(syscallMock.WasMountCalledWith( "", @@ -161,20 +186,207 @@ var _ = Describe("Bootentries tests", Label("bootentry"), func() { syscall.MS_REMOUNT|syscall.MS_RDONLY, "")).To(BeTrue()) }) - It("selects the boot entry with the missing .conf extension", func() { - err := fs.WriteFile("/efi/loader/entries/kairos.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) + 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/kairos2.conf", []byte("title kairos\nlinux /vmlinuz\ninitrd /initrd\noptions root=LABEL=COS_GRUB\n"), os.ModePerm) + 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) + 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()) err = fs.WriteFile("/efi/loader/loader.conf", []byte(""), os.ModePerm) - - err = SelectBootEntry(config, "kairos2") Expect(err).ToNot(HaveOccurred()) - Expect(memLog.String()).To(ContainSubstring("Default boot entry set to kairos2")) + + 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("kairos2.conf")) + Expect(reader["default"]).To(Equal("passive_install-mode_awesomeos.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()) + err = SelectBootEntry(config, "recovery") + Expect(err).ToNot(HaveOccurred()) + 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_install-mode_awesomeos.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()) + + err = SelectBootEntry(config, "cos") + Expect(err).ToNot(HaveOccurred()) + 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_install-mode_awesomeos.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 extra-cmdline installation", 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/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) + 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) + 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/loader.conf", []byte(""), 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")) + // 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()) + + err = SelectBootEntry(config, "fallback foobar") + Expect(err).ToNot(HaveOccurred()) + Expect(memLog.String()).To(ContainSubstring("Default boot entry set to fallback foobar")) + reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf") + Expect(err).ToNot(HaveOccurred()) + Expect(reader["default"]).To(Equal("passive_foobar.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()) + + err = SelectBootEntry(config, "recovery") + Expect(err).ToNot(HaveOccurred()) + 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")) + // 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()) + + err = SelectBootEntry(config, "recovery foobar") + Expect(err).ToNot(HaveOccurred()) + Expect(memLog.String()).To(ContainSubstring("Default boot entry set to recovery foobar")) + reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf") + Expect(err).ToNot(HaveOccurred()) + Expect(reader["default"]).To(Equal("recovery_foobar.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()) + + err = SelectBootEntry(config, "cos") + Expect(err).ToNot(HaveOccurred()) + 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")) + // 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()) + + err = SelectBootEntry(config, "cos foobar") + Expect(err).ToNot(HaveOccurred()) + Expect(memLog.String()).To(ContainSubstring("Default boot entry set to cos foobar")) + reader, err = utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf") + Expect(err).ToNot(HaveOccurred()) + Expect(reader["default"]).To(Equal("active_foobar.conf")) // Should have called a remount to make it RW Expect(syscallMock.WasMountCalledWith( "",