-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add shim to choose next entry to boot from (#230)
- Loading branch information
Showing
13 changed files
with
775 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package action | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
"syscall" | ||
|
||
"github.com/erikgeiser/promptkit/confirmation" | ||
"github.com/erikgeiser/promptkit/selection" | ||
"github.com/kairos-io/kairos-agent/v2/pkg/config" | ||
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" | ||
"github.com/kairos-io/kairos-agent/v2/pkg/utils" | ||
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" | ||
"github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" | ||
) | ||
|
||
// SelectBootEntry sets the default boot entry to the selected entry | ||
// This is the entrypoint for the bootentry action with --select flag | ||
// also other actions can call this function to set the default boot entry | ||
func SelectBootEntry(cfg *config.Config, entry string) error { | ||
if utils.IsUkiWithFs(cfg.Fs) { | ||
return selectBootEntrySystemd(cfg, entry) | ||
} else { | ||
return selectBootEntryGrub(cfg, entry) | ||
} | ||
} | ||
|
||
// ListBootEntries lists the boot entries available in the system and prompts the user to select one | ||
// then calls the underlying SelectBootEntry function to mange the entry writing and validation | ||
func ListBootEntries(cfg *config.Config) error { | ||
if utils.IsUkiWithFs(cfg.Fs) { | ||
return listBootEntriesSystemd(cfg) | ||
} else { | ||
return listBootEntriesGrub(cfg) | ||
} | ||
} | ||
|
||
// selectBootEntryGrub sets the default boot entry to the selected entry via `grub2-editenv /oem/grubenv set next_entry=entry` | ||
// also validates that the entry exists in our list of entries | ||
func selectBootEntryGrub(cfg *config.Config, entry string) error { | ||
// Validate if entry exists | ||
entries, err := listGrubEntries(cfg) | ||
if err != nil { | ||
return err | ||
} | ||
// Check that entry exists in the entries list | ||
err = entryInList(cfg, entry, entries) | ||
if err != nil { | ||
return err | ||
} | ||
cfg.Logger.Infof("Setting default boot entry to %s", entry) | ||
// Set the default entry to the selected entry via `grub2-editenv /oem/grubenv set next_entry=statereset` | ||
out, err := cfg.Runner.Run("grub2-editenv", "/oem/grubenv", "set", fmt.Sprintf("next_entry=%s", entry)) | ||
if err != nil { | ||
cfg.Logger.Errorf("could not set default boot entry: %s\noutput: %s", err, out) | ||
return err | ||
} | ||
cfg.Logger.Infof("Default boot entry set to %s", entry) | ||
return err | ||
} | ||
|
||
// 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 | ||
efiPartition, err := partitions.GetEfiPartition() | ||
if err != nil { | ||
return err | ||
} | ||
// Validate entry exists | ||
entries, err := listSystemdEntries(cfg, efiPartition) | ||
if err != nil { | ||
return err | ||
|
||
} | ||
// Check that entry exists in the entries list | ||
err = entryInList(cfg, entry, entries) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Mount it RW | ||
err = cfg.Syscall.Mount("", efiPartition.MountPoint, "", syscall.MS_REMOUNT, "") | ||
if err != nil { | ||
cfg.Logger.Errorf("could not remount EFI partition: %s", err) | ||
return err | ||
} | ||
// Remount it RO when finished | ||
defer func(source string, target string, fstype string, flags uintptr, data string) { | ||
err = cfg.Syscall.Mount(source, target, fstype, flags, data) | ||
if err != nil { | ||
cfg.Logger.Errorf("could not remount EFI partition as RO: %s", err) | ||
} | ||
}("", efiPartition.MountPoint, "", syscall.MS_REMOUNT|syscall.MS_RDONLY, "") | ||
|
||
systemdConf, err := utils.SystemdBootConfReader(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/loader.conf")) | ||
if err != nil { | ||
cfg.Logger.Errorf("could not read loader.conf: %s", err) | ||
return err | ||
} | ||
// Set the default entry to the selected entry | ||
systemdConf["default"] = entry | ||
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) | ||
return err | ||
} | ||
cfg.Logger.Infof("Default boot entry set to %s", entry) | ||
return err | ||
} | ||
|
||
// listBootEntriesGrub lists the boot entries available in the grub config files | ||
// and prompts the user to select one | ||
// then calls the underlying SelectBootEntry function to mange the entry writing and validation | ||
func listBootEntriesGrub(cfg *config.Config) error { | ||
entries, err := listGrubEntries(cfg) | ||
if err != nil { | ||
return err | ||
} | ||
// create a selector | ||
selector := selection.New("Select Next Boot Entry", 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 confirm { | ||
return SelectBootEntry(cfg, selected) | ||
} | ||
return err | ||
} | ||
|
||
// 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 | ||
func listBootEntriesSystemd(cfg *config.Config) error { | ||
// Get EFI partition | ||
efiPartition, err := partitions.GetEfiPartition() | ||
if err != nil { | ||
return err | ||
} | ||
// Get default entry from loader.conf | ||
loaderConf, err := utils.SystemdBootConfReader(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/loader.conf")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
entries, err := listSystemdEntries(cfg, efiPartition) | ||
|
||
// create a selector | ||
selector := selection.New(fmt.Sprintf("Select Boot Entry (current entry: %s)", loaderConf["default"]), 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 confirm { | ||
return SelectBootEntry(cfg, selected) | ||
} | ||
return err | ||
} | ||
|
||
// ListSystemdEntries reads the systemd-boot entries and returns a list of entries found | ||
func listSystemdEntries(cfg *config.Config, efiPartition *v1.Partition) ([]string, error) { | ||
var entries []string | ||
err := fsutils.WalkDirFs(cfg.Fs, filepath.Join(efiPartition.MountPoint, "loader/entries/"), func(path string, info os.DirEntry, err error) error { | ||
cfg.Logger.Debugf("Checking file %s", path) | ||
if info == nil { | ||
return nil | ||
} | ||
if info.IsDir() { | ||
return nil | ||
} | ||
if filepath.Ext(info.Name()) != ".conf" { | ||
return nil | ||
} | ||
entries = append(entries, info.Name()) | ||
return nil | ||
}) | ||
return entries, err | ||
} | ||
|
||
// listGrubEntries reads the grub config files and returns a list of entries found | ||
func listGrubEntries(cfg *config.Config) ([]string, error) { | ||
// Read grub config from 3 places | ||
// /etc/cos/grub.cfg | ||
// /run/initramfs/cos-state/grub/grub.cfg | ||
// /etc/kairos/branding/grubmenu.cfg | ||
// And grep the entries by checking the --id\s([A-z0-9]*)\s{ pattern | ||
var entries []string | ||
for _, file := range []string{"/etc/cos/grub.cfg", "/run/initramfs/cos-state/grub/grub.cfg", "/etc/kairos/branding/grubmenu.cfg"} { | ||
f, err := cfg.Fs.ReadFile(file) | ||
if err != nil { | ||
cfg.Logger.Errorf("could not read file %s: %s", file, err) | ||
continue | ||
} | ||
re, _ := regexp.Compile(`--id\s([A-z0-9]*)\s{`) | ||
matches := re.FindAllStringSubmatch(string(f), -1) | ||
for _, match := range matches { | ||
entries = append(entries, match[1]) | ||
} | ||
} | ||
entries = uniqueStringArray(entries) | ||
return entries, nil | ||
} | ||
|
||
// I lost count on how many places I had to implement this function | ||
// Is that difficult to provide a simple []string.Unique() method from the standard lib???? | ||
// Or at least a Set type? | ||
func uniqueStringArray(arr []string) []string { | ||
unique := make(map[string]bool) | ||
var result []string | ||
for _, s := range arr { | ||
if !unique[s] { | ||
unique[s] = true | ||
result = append(result, s) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
// Another one. Seriously there is nothing to check if something is in a list? | ||
func entryInList(cfg *config.Config, entry string, list []string) error { | ||
// Check that entry exists in the entries list | ||
for _, e := range list { | ||
if e == entry { | ||
return nil | ||
} | ||
} | ||
cfg.Logger.Errorf("entry %s does not exist", entry) | ||
cfg.Logger.Debugf("entries: %v", list) | ||
return fmt.Errorf("entry %s does not exist", entry) | ||
} |
Oops, something went wrong.