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 <mauro.morales@spectrocloud.com>

* Extended cmdline

Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>

* Extend tests

Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>

---------

Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
This commit is contained in:
Mauro Morales 2024-03-15 09:17:37 +01:00 committed by GitHub
parent 8b6dde816c
commit 027a8800c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 338 additions and 28 deletions

View File

@ -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

View File

@ -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(
"",