mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-07-12 15:38:03 +00:00
Add shim to choose next entry to boot from (#230)
This commit is contained in:
parent
cce432133e
commit
2e9c85e63a
@ -219,7 +219,7 @@ func RunInstall(c *config.Config) error {
|
|||||||
|
|
||||||
// UKI path. Check if we are on UKI AND if we are running off a cd, otherwise it makes no sense to run the install
|
// UKI path. Check if we are on UKI AND if we are running off a cd, otherwise it makes no sense to run the install
|
||||||
// From the installed system
|
// From the installed system
|
||||||
if internalutils.IsUki() {
|
if internalutils.IsUkiWithFs(c.Fs) {
|
||||||
c.Logger.Debugf("UKI mode: %s\n", internalutils.UkiBootMode())
|
c.Logger.Debugf("UKI mode: %s\n", internalutils.UkiBootMode())
|
||||||
if internalutils.UkiBootMode() == internalutils.UkiRemovableMedia {
|
if internalutils.UkiBootMode() == internalutils.UkiRemovableMedia {
|
||||||
return runInstallUki(c)
|
return runInstallUki(c)
|
||||||
|
53
main.go
53
main.go
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -36,15 +37,6 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configScanDir = []string{
|
|
||||||
"/oem",
|
|
||||||
"/system/oem",
|
|
||||||
"/usr/local/cloud-config",
|
|
||||||
"/run/initramfs/live",
|
|
||||||
"/etc/kairos", // Default system configuration file https://github.com/kairos-io/kairos/issues/2221
|
|
||||||
"/etc/elemental", // for backwards compatibility
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleasesToOutput gets a semver.Collection and outputs it in the given format
|
// ReleasesToOutput gets a semver.Collection and outputs it in the given format
|
||||||
// Only used here.
|
// Only used here.
|
||||||
func ReleasesToOutput(rels []string, output string) []string {
|
func ReleasesToOutput(rels []string, output string) []string {
|
||||||
@ -186,7 +178,7 @@ See https://kairos.io/docs/upgrade/manual/ for documentation.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return agent.Upgrade(source, c.Bool("force"),
|
return agent.Upgrade(source, c.Bool("force"),
|
||||||
c.Bool("strict-validation"), configScanDir,
|
c.Bool("strict-validation"), constants.GetConfigScanDirs(),
|
||||||
c.Bool("pre"), c.Bool("recovery"),
|
c.Bool("pre"), c.Bool("recovery"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -330,7 +322,7 @@ E.g. kairos-agent install-bundle container:quay.io/kairos/kairos...
|
|||||||
Description: "Show the runtime configuration of the machine. It will scan the machine for all the configuration and will return the config file processed and found.",
|
Description: "Show the runtime configuration of the machine. It will scan the machine for all the configuration and will return the config file processed and found.",
|
||||||
Aliases: []string{},
|
Aliases: []string{},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs)
|
config, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -359,7 +351,7 @@ enabled: true`,
|
|||||||
Description: "It allows to navigate the YAML config file by searching with 'yq' style keywords as `config get k3s` to retrieve the k3s config block",
|
Description: "It allows to navigate the YAML config file by searching with 'yq' style keywords as `config get k3s` to retrieve the k3s config block",
|
||||||
Aliases: []string{"g"},
|
Aliases: []string{"g"},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
|
config, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -430,7 +422,7 @@ enabled: true`,
|
|||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
|
||||||
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
|
config, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs, collector.StrictValidation(c.Bool("strict-validation")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -540,7 +532,7 @@ This command is meant to be used from the boot GRUB menu, but can be started man
|
|||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
source := c.String("source")
|
source := c.String("source")
|
||||||
|
|
||||||
return agent.Install(source, configScanDir...)
|
return agent.Install(source, constants.GetConfigScanDirs()...)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -584,7 +576,7 @@ This command is meant to be used from the boot GRUB menu, but can likely be used
|
|||||||
unattended := c.Bool("unattended")
|
unattended := c.Bool("unattended")
|
||||||
resetOem := c.Bool("reset-oem")
|
resetOem := c.Bool("reset-oem")
|
||||||
|
|
||||||
return agent.Reset(reboot, unattended, resetOem, configScanDir...)
|
return agent.Reset(reboot, unattended, resetOem, constants.GetConfigScanDirs()...)
|
||||||
},
|
},
|
||||||
Usage: "Starts kairos reset mode",
|
Usage: "Starts kairos reset mode",
|
||||||
Description: `
|
Description: `
|
||||||
@ -663,7 +655,7 @@ The validate command expects a configuration file as its only argument. Local fi
|
|||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
stage := c.Args().First()
|
stage := c.Args().First()
|
||||||
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs)
|
config, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs)
|
||||||
config.Strict = c.Bool("strict")
|
config.Strict = c.Bool("strict")
|
||||||
|
|
||||||
if len(c.StringSlice("cloud-init-paths")) > 0 {
|
if len(c.StringSlice("cloud-init-paths")) > 0 {
|
||||||
@ -706,7 +698,7 @@ The validate command expects a configuration file as its only argument. Local fi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid path %s", destination)
|
return fmt.Errorf("invalid path %s", destination)
|
||||||
}
|
}
|
||||||
config, err := agentConfig.Scan(collector.Directories(configScanDir...), collector.NoLogs)
|
config, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -745,6 +737,33 @@ The validate command expects a configuration file as its only argument. Local fi
|
|||||||
Description: "versioneer subcommands",
|
Description: "versioneer subcommands",
|
||||||
Subcommands: versioneer.CliCommands(),
|
Subcommands: versioneer.CliCommands(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "bootentry",
|
||||||
|
Usage: "bootentry [--select]",
|
||||||
|
Description: "bootentry subcommands",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "select",
|
||||||
|
Usage: "Select the boot entry",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Before: func(c *cli.Context) error {
|
||||||
|
return checkRoot()
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
cfg, err := agentConfig.Scan(collector.Directories(constants.GetConfigScanDirs()...), collector.NoLogs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := c.String("select")
|
||||||
|
// If we got a selection just go for it, otherwise enter an interactive mode to show entries and let user choose one
|
||||||
|
if s != "" {
|
||||||
|
return action.SelectBootEntry(cfg, s)
|
||||||
|
}
|
||||||
|
return action.ListBootEntries(cfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
245
pkg/action/bootentries.go
Normal file
245
pkg/action/bootentries.go
Normal file
@ -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)
|
||||||
|
}
|
258
pkg/action/bootentries_test.go
Normal file
258
pkg/action/bootentries_test.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"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"
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/utils"
|
||||||
|
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
||||||
|
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
|
||||||
|
"github.com/kairos-io/kairos-sdk/collector"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/twpayne/go-vfs"
|
||||||
|
"github.com/twpayne/go-vfs/vfst"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Bootentries tests", Label("bootentry"), func() {
|
||||||
|
var config *agentConfig.Config
|
||||||
|
var fs vfs.FS
|
||||||
|
var logger v1.Logger
|
||||||
|
var runner *v1mock.FakeRunner
|
||||||
|
var mounter *v1mock.ErrorMounter
|
||||||
|
var syscallMock *v1mock.FakeSyscall
|
||||||
|
var client *v1mock.FakeHTTPClient
|
||||||
|
var cloudInit *v1mock.FakeCloudInitRunner
|
||||||
|
var cleanup func()
|
||||||
|
var memLog *bytes.Buffer
|
||||||
|
var extractor *v1mock.FakeImageExtractor
|
||||||
|
var ghwTest v1mock.GhwMock
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
runner = v1mock.NewFakeRunner()
|
||||||
|
syscallMock = &v1mock.FakeSyscall{}
|
||||||
|
mounter = v1mock.NewErrorMounter()
|
||||||
|
client = &v1mock.FakeHTTPClient{}
|
||||||
|
memLog = &bytes.Buffer{}
|
||||||
|
logger = v1.NewBufferLogger(memLog)
|
||||||
|
extractor = v1mock.NewFakeImageExtractor(logger)
|
||||||
|
logger.SetLevel(v1.DebugLevel())
|
||||||
|
var err error
|
||||||
|
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{})
|
||||||
|
// Create proper dir structure for our EFI partition contentens
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/efi/loader/entries", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/efi/EFI/BOOT", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/efi/EFI/kairos", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/etc/cos/", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/run/initramfs/cos-state/grub/", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
err = fsutils.MkdirAll(fs, "/etc/kairos/branding/", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).Should(BeNil())
|
||||||
|
|
||||||
|
cloudInit = &v1mock.FakeCloudInitRunner{}
|
||||||
|
config = agentConfig.NewConfig(
|
||||||
|
agentConfig.WithFs(fs),
|
||||||
|
agentConfig.WithRunner(runner),
|
||||||
|
agentConfig.WithLogger(logger),
|
||||||
|
agentConfig.WithMounter(mounter),
|
||||||
|
agentConfig.WithSyscall(syscallMock),
|
||||||
|
agentConfig.WithClient(client),
|
||||||
|
agentConfig.WithCloudInitRunner(cloudInit),
|
||||||
|
agentConfig.WithImageExtractor(extractor),
|
||||||
|
)
|
||||||
|
config.Config = collector.Config{}
|
||||||
|
|
||||||
|
mainDisk := block.Disk{
|
||||||
|
Name: "device",
|
||||||
|
Partitions: []*block.Partition{
|
||||||
|
{
|
||||||
|
Name: "device1",
|
||||||
|
FilesystemLabel: "COS_GRUB",
|
||||||
|
Type: "ext4",
|
||||||
|
MountPoint: "/efi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ghwTest = v1mock.GhwMock{}
|
||||||
|
ghwTest.AddDisk(mainDisk)
|
||||||
|
ghwTest.CreateDevices()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
Context("Under Uki", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
err := fs.Mkdir("/proc", os.ModeDir|os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/proc/cmdline", []byte("rd.immucore.uki"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
|
Context("ListBootEntries", func() {
|
||||||
|
It("fails to list the boot entries when there is no loader.conf", func() {
|
||||||
|
err := ListBootEntries(config)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("ListSystemdEntries", 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)
|
||||||
|
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)
|
||||||
|
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"))
|
||||||
|
|
||||||
|
})
|
||||||
|
It("list empty boot entries if there is none", func() {
|
||||||
|
entries, err := listSystemdEntries(config, &v1.Partition{MountPoint: "/efi"})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(entries).To(HaveLen(0))
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("SelectBootEntry", func() {
|
||||||
|
It("fails to select the boot entry if it doesnt exist", func() {
|
||||||
|
err := SelectBootEntry(config, "kairos")
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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"))
|
||||||
|
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(reader["default"]).To(Equal("kairos.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 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)
|
||||||
|
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)
|
||||||
|
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"))
|
||||||
|
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/loader.conf")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(reader["default"]).To(Equal("kairos2.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())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("Under grub", func() {
|
||||||
|
Context("ListBootEntries", func() {
|
||||||
|
It("fails to list the boot entries when there is no grub files", func() {
|
||||||
|
err := ListBootEntries(config)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("ListSystemdEntries", func() {
|
||||||
|
It("lists the boot entries if there is any", func() {
|
||||||
|
err := fs.WriteFile("/etc/cos/grub.cfg", []byte("whatever whatever --id kairos {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/run/initramfs/cos-state/grub/grub.cfg", []byte("whatever whatever --id kairos2 {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/etc/kairos/branding/grubmenu.cfg", []byte("whatever whatever --id kairos3 {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
entries, err := listGrubEntries(config)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(entries).To(HaveLen(3))
|
||||||
|
Expect(entries).To(ContainElement("kairos"))
|
||||||
|
Expect(entries).To(ContainElement("kairos2"))
|
||||||
|
Expect(entries).To(ContainElement("kairos3"))
|
||||||
|
|
||||||
|
})
|
||||||
|
It("list empty boot entries if there is none", func() {
|
||||||
|
entries, err := listGrubEntries(config)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(entries).To(HaveLen(0))
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Context("SelectBootEntry", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
runner.SideEffect = func(cmd string, args ...string) ([]byte, error) {
|
||||||
|
switch cmd {
|
||||||
|
case "grub2-editenv":
|
||||||
|
return []byte(""), nil
|
||||||
|
default:
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
It("fails to select the boot entry if it doesnt exist", func() {
|
||||||
|
err := SelectBootEntry(config, "kairos")
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("does not exist"))
|
||||||
|
})
|
||||||
|
It("selects the boot entry", func() {
|
||||||
|
err := fs.WriteFile("/etc/cos/grub.cfg", []byte("whatever whatever --id kairos {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/run/initramfs/cos-state/grub/grub.cfg", []byte("whatever whatever --id kairos2 {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/etc/kairos/branding/grubmenu.cfg", []byte("whatever whatever --id kairos3 {"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = SelectBootEntry(config, "kairos")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(runner.IncludesCmds([][]string{
|
||||||
|
{"grub2-editenv", "/oem/grubenv", "set", "next_entry=kairos"},
|
||||||
|
})).ToNot(HaveOccurred())
|
||||||
|
Expect(memLog.String()).To(ContainSubstring("Default boot entry set to kairos"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -149,3 +149,14 @@ func GetGrubFonts() []string {
|
|||||||
func GetGrubModules() []string {
|
func GetGrubModules() []string {
|
||||||
return []string{"loopback.mod", "squash4.mod", "xzio.mod", "gzio.mod", "regexp.mod"}
|
return []string{"loopback.mod", "squash4.mod", "xzio.mod", "gzio.mod", "regexp.mod"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetConfigScanDirs() []string {
|
||||||
|
return []string{
|
||||||
|
"/oem",
|
||||||
|
"/system/oem",
|
||||||
|
"/usr/local/cloud-config",
|
||||||
|
"/run/initramfs/live",
|
||||||
|
"/etc/kairos", // Default system configuration file https://github.com/kairos-io/kairos/issues/2221
|
||||||
|
"/etc/elemental", // for backwards compatibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
type SyscallInterface interface {
|
type SyscallInterface interface {
|
||||||
Chroot(string) error
|
Chroot(string) error
|
||||||
Chdir(string) error
|
Chdir(string) error
|
||||||
|
Mount(string, string, string, uintptr, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealSyscall struct{}
|
type RealSyscall struct{}
|
||||||
@ -34,3 +35,6 @@ func (r *RealSyscall) Chroot(path string) error {
|
|||||||
func (r *RealSyscall) Chdir(path string) error {
|
func (r *RealSyscall) Chdir(path string) error {
|
||||||
return syscall.Chdir(path)
|
return syscall.Chdir(path)
|
||||||
}
|
}
|
||||||
|
func (r *RealSyscall) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||||
|
return syscall.Mount(source, target, fstype, flags, data)
|
||||||
|
}
|
||||||
|
@ -44,4 +44,14 @@ var _ = Describe("Syscall", Label("types", "syscall"), func() {
|
|||||||
// We need elevated privs to chroot so this should fail
|
// We need elevated privs to chroot so this should fail
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
})
|
})
|
||||||
|
It("Calling mount on the fake syscall should not fail", func() {
|
||||||
|
r := v1mock.FakeSyscall{}
|
||||||
|
err := r.Mount("source", "target", "fstype", 0, "data")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
})
|
||||||
|
It("Calling mount on the real syscall fail (wrong args)", func() {
|
||||||
|
r := v1.RealSyscall{}
|
||||||
|
err := r.Mount("source", "target", "fstype", 0, "data")
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,6 +3,7 @@ package uki
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/action"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
|
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
|
||||||
@ -70,6 +71,12 @@ func (r *ResetAction) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copying recovery to active: %w", err)
|
return fmt.Errorf("copying recovery to active: %w", err)
|
||||||
}
|
}
|
||||||
|
// SelectBootEntry sets the default boot entry to the selected entry
|
||||||
|
err = action.SelectBootEntry(r.cfg, "active")
|
||||||
|
// Should we fail? Or warn?
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
_ = elementalUtils.RunStage(r.cfg, "kairos-uki-reset.after")
|
_ = elementalUtils.RunStage(r.cfg, "kairos-uki-reset.after")
|
||||||
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.reset.after.hook") //nolint:errcheck
|
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.reset.after.hook") //nolint:errcheck
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/action"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
|
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
|
||||||
@ -77,6 +78,13 @@ func (i *UpgradeAction) Run() (err error) {
|
|||||||
return fmt.Errorf("removing artifact set: %w", err)
|
return fmt.Errorf("removing artifact set: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectBootEntry sets the default boot entry to the selected entry
|
||||||
|
err = action.SelectBootEntry(i.cfg, "active")
|
||||||
|
// Should we fail? Or warn?
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
_ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after")
|
_ = elementalUtils.RunStage(i.cfg, "kairos-uki-upgrade.after")
|
||||||
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck
|
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.upgrade.after.hook") //nolint:errcheck
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -510,6 +511,17 @@ func IsUki() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsUkiWithFs checks if the system is running in UKI mode
|
||||||
|
// by checking the kernel command line for the rd.immucore.uki flag
|
||||||
|
// Uses a v1.Fs interface to allow for testing
|
||||||
|
func IsUkiWithFs(fs v1.FS) bool {
|
||||||
|
cmdline, _ := fs.ReadFile("/proc/cmdline")
|
||||||
|
if strings.Contains(string(cmdline), "rd.immucore.uki") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UkiHDD state.Boot = "uki_boot_mode"
|
UkiHDD state.Boot = "uki_boot_mode"
|
||||||
UkiRemovableMedia state.Boot = "uki_install_mode"
|
UkiRemovableMedia state.Boot = "uki_install_mode"
|
||||||
@ -528,3 +540,60 @@ func UkiBootMode() state.Boot {
|
|||||||
}
|
}
|
||||||
return state.Unknown
|
return state.Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemdBootConfReader reads a systemd-boot conf file and returns a map with the key/value pairs
|
||||||
|
// TODO: Move this to the sdk with the FS interface
|
||||||
|
func SystemdBootConfReader(fs v1.FS, filePath string) (map[string]string, error) {
|
||||||
|
file, err := fs.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
_ = file.Close()
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
result := make(map[string]string)
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
result[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
result[parts[0]] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemdBootConfWriter writes a map to a systemd-boot conf file
|
||||||
|
// TODO: Move this to the sdk with the FS interface
|
||||||
|
func SystemdBootConfWriter(fs v1.FS, filePath string, conf map[string]string) error {
|
||||||
|
file, err := fs.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
_ = file.Close()
|
||||||
|
}(file)
|
||||||
|
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
for k, v := range conf {
|
||||||
|
if v == "" {
|
||||||
|
_, err = writer.WriteString(fmt.Sprintf("%s \n", k))
|
||||||
|
} else {
|
||||||
|
_, err = writer.WriteString(fmt.Sprintf("%s %s\n", k, v))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/jaypipes/ghw/pkg/context"
|
"github.com/jaypipes/ghw/pkg/context"
|
||||||
"github.com/jaypipes/ghw/pkg/linuxpath"
|
"github.com/jaypipes/ghw/pkg/linuxpath"
|
||||||
ghwUtil "github.com/jaypipes/ghw/pkg/util"
|
ghwUtil "github.com/jaypipes/ghw/pkg/util"
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -209,3 +210,22 @@ func GetPartitionViaDM(fs v1.FS, label string) *v1.Partition {
|
|||||||
}
|
}
|
||||||
return part
|
return part
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEfiPartition returns the EFI partition by looking for the partition with the label "COS_GRUB"
|
||||||
|
func GetEfiPartition() (*v1.Partition, error) {
|
||||||
|
var efiPartition *v1.Partition
|
||||||
|
parts, err := GetAllPartitions()
|
||||||
|
if err != nil {
|
||||||
|
return efiPartition, fmt.Errorf("could not read host partitions")
|
||||||
|
}
|
||||||
|
for _, p := range parts {
|
||||||
|
if p.FilesystemLabel == constants.EfiLabel {
|
||||||
|
efiPartition = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if efiPartition == nil {
|
||||||
|
return efiPartition, fmt.Errorf("could not find EFI partition")
|
||||||
|
}
|
||||||
|
return efiPartition, nil
|
||||||
|
}
|
||||||
|
@ -1071,4 +1071,78 @@ var _ = Describe("Utils", Label("utils"), func() {
|
|||||||
Expect(err.Error()).To(ContainSubstring("Cleanup error 3"))
|
Expect(err.Error()).To(ContainSubstring("Cleanup error 3"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Describe("GetEfiPartition", func() {
|
||||||
|
var ghwTest v1mock.GhwMock
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
mainDisk := block.Disk{
|
||||||
|
Name: "device",
|
||||||
|
Partitions: []*block.Partition{
|
||||||
|
{
|
||||||
|
Name: "device1",
|
||||||
|
FilesystemLabel: "COS_GRUB",
|
||||||
|
Type: "ext4",
|
||||||
|
MountPoint: "/efi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ghwTest = v1mock.GhwMock{}
|
||||||
|
ghwTest.AddDisk(mainDisk)
|
||||||
|
ghwTest.CreateDevices()
|
||||||
|
})
|
||||||
|
It("returns the efi partition", func() {
|
||||||
|
efi, err := partitions.GetEfiPartition()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(efi.FilesystemLabel).To(Equal("COS_GRUB"))
|
||||||
|
Expect(efi.Name).To(Equal("device1")) // Just to make sure its our mocked system
|
||||||
|
})
|
||||||
|
It("fails to find the efi partition", func() {
|
||||||
|
ghwTest.Clean() // Remove the disk
|
||||||
|
efi, err := partitions.GetEfiPartition()
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(efi).To(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Describe("SystemdBootConfWriter/SystemdBootConfReader", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
err := fsutils.MkdirAll(fs, "/efi/loader/entries", os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
|
It("writes the conf file with proper attrs", func() {
|
||||||
|
conf := map[string]string{
|
||||||
|
"timeout": "5",
|
||||||
|
"default": "kairos",
|
||||||
|
"empty": "",
|
||||||
|
}
|
||||||
|
err := utils.SystemdBootConfWriter(fs, "/efi/loader/entries/test1.conf", conf)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/entries/test1.conf")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(reader["timeout"]).To(Equal("5"))
|
||||||
|
Expect(reader["default"]).To(Equal("kairos"))
|
||||||
|
Expect(reader["recovery"]).To(Equal(""))
|
||||||
|
})
|
||||||
|
It("reads the conf file with proper k,v attrs", func() {
|
||||||
|
err := fs.WriteFile("/efi/loader/entries/test2.conf", []byte("timeout 5\ndefault kairos\nrecovery\n"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
reader, err := utils.SystemdBootConfReader(fs, "/efi/loader/entries/test2.conf")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(reader["timeout"]).To(Equal("5"))
|
||||||
|
Expect(reader["default"]).To(Equal("kairos"))
|
||||||
|
Expect(reader["recovery"]).To(Equal(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
Describe("IsUkiWithFs", func() {
|
||||||
|
It("returns true if rd.immucore.uki is present", func() {
|
||||||
|
err := fs.Mkdir("/proc", os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/proc/cmdline", []byte("rd.immucore.uki"), os.ModePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(utils.IsUkiWithFs(fs)).To(BeTrue())
|
||||||
|
})
|
||||||
|
It("returns false if rd.immucore.uki is not present", func() {
|
||||||
|
Expect(utils.IsUkiWithFs(fs)).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -16,13 +16,24 @@ limitations under the License.
|
|||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
// FakeSyscall is a test helper method to track calls to syscall
|
// FakeSyscall is a test helper method to track calls to syscall
|
||||||
// It can also fail on Chroot command
|
// It can also fail on Chroot command
|
||||||
type FakeSyscall struct {
|
type FakeSyscall struct {
|
||||||
chrootHistory []string // Track calls to chroot
|
chrootHistory []string // Track calls to chroot
|
||||||
ErrorOnChroot bool
|
ErrorOnChroot bool
|
||||||
|
mounts []FakeMount
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeMount struct {
|
||||||
|
Source string
|
||||||
|
Target string
|
||||||
|
Fstype string
|
||||||
|
Flags uintptr
|
||||||
|
Data string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chroot will store the chroot call
|
// Chroot will store the chroot call
|
||||||
@ -48,3 +59,23 @@ func (f *FakeSyscall) WasChrootCalledWith(path string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeSyscall) Mount(source string, target string, fstype string, flags uintptr, data string) error {
|
||||||
|
f.mounts = append(f.mounts, FakeMount{
|
||||||
|
Source: source,
|
||||||
|
Target: target,
|
||||||
|
Fstype: fstype,
|
||||||
|
Flags: flags,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSyscall) WasMountCalledWith(source string, target string, fstype string, flags uintptr, data string) bool {
|
||||||
|
for _, m := range f.mounts {
|
||||||
|
if m.Source == source && m.Target == target && m.Fstype == fstype && m.Flags == flags && m.Data == data {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user