mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-03 18:16:50 +00:00
We were basically overwriting the file if it existed which is bad. Now it will read the file if it exists and ingest the existing ones, then in will override witht he given vars if they match, warn the user and then store the full processed vars. This means it will never overwrite any vars again Signed-off-by: Itxaka <itxaka@kairos.io>
480 lines
18 KiB
Go
480 lines
18 KiB
Go
/*
|
|
Copyright © 2022 SUSE LLC
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package utils
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
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/fs"
|
|
"github.com/kairos-io/kairos-sdk/utils"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
|
cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
|
)
|
|
|
|
// Grub is the struct that will allow us to install grub to the target device
|
|
type Grub struct {
|
|
config *agentConfig.Config
|
|
}
|
|
|
|
func NewGrub(config *agentConfig.Config) *Grub {
|
|
g := &Grub{
|
|
config: config,
|
|
}
|
|
|
|
return g
|
|
}
|
|
|
|
// Install installs grub into the device, copy the config file and add any extra TTY to grub
|
|
// TODO: Make it more generic to be able to call it from other places
|
|
// i.e.: filepath.Join(cnst.ActiveDir, "etc/kairos-release") seraches for the file in the active dir, we should be looking into the rootdir?
|
|
// filepath.Join(cnst.EfiDir, "EFI/boot/grub.cfg") we also write into the efi dir directly, this should be the a var maybe?
|
|
func (g Grub) Install(target, rootDir, bootDir, grubConf, tty string, efi bool, stateLabel string) (err error) { // nolint:gocyclo
|
|
var grubargs []string
|
|
var grubdir, finalContent string
|
|
|
|
// At this point the active mountpoint has all the data from the installation source, so we should be able to use
|
|
// the grub.cfg bundled in there
|
|
systemgrub := "grub2"
|
|
|
|
// only install grub on non-efi systems
|
|
if !efi {
|
|
g.config.Logger.Info("Installing GRUB..")
|
|
|
|
// Find where in the rootDir the grub2 files for i386-pc are
|
|
// Use the modinfo.sh as a marker
|
|
grubdir = findGrubDir(g.config.Fs, rootDir)
|
|
if grubdir == "" {
|
|
g.config.Logger.Logger.Error().Str("path", rootDir).Msg("Failed to find grub dir")
|
|
return fmt.Errorf("failed to find grub dir under %s", rootDir)
|
|
}
|
|
|
|
grubargs = append(
|
|
grubargs,
|
|
fmt.Sprintf("--directory=%s", grubdir),
|
|
fmt.Sprintf("--boot-directory=%s", bootDir),
|
|
"--target=i386-pc",
|
|
target,
|
|
)
|
|
|
|
g.config.Logger.Debugf("Running grub with the following args: %s", grubargs)
|
|
grubBin := FindCommand("grub2-install", []string{
|
|
filepath.Join(rootDir, "/usr/sbin/", "grub2-install"),
|
|
filepath.Join(rootDir, "/usr/bin/", "grub2-install"),
|
|
filepath.Join(rootDir, "/sbin/", "grub2-install"),
|
|
filepath.Join(rootDir, "/usr/sbin/", "grub-install"),
|
|
filepath.Join(rootDir, "/usr/bin/", "grub-install"),
|
|
filepath.Join(rootDir, "/sbin/", "grub-install"),
|
|
})
|
|
g.config.Logger.Logger.Warn().Str("command", grubBin).Msg("Found grub binary")
|
|
if grubBin == "" {
|
|
g.config.Logger.Logger.Error().Str("path", rootDir).Msg("Grub binary not found in path")
|
|
return fmt.Errorf("grub binary not found in path")
|
|
}
|
|
out, err := g.config.Runner.Run(grubBin, grubargs...)
|
|
if err != nil {
|
|
g.config.Logger.Errorf(string(out))
|
|
return err
|
|
}
|
|
g.config.Logger.Infof("Grub install to device %s complete", target)
|
|
|
|
// Select the proper dir for grub - this assumes that we previously run a grub install command, which is not the case in EFI
|
|
// In the EFI case we default to grub2
|
|
if ok, _ := fsutils.IsDir(g.config.Fs, filepath.Join(bootDir, "grub")); ok {
|
|
systemgrub = "grub"
|
|
}
|
|
}
|
|
|
|
grubdir = filepath.Join(rootDir, grubConf)
|
|
g.config.Logger.Infof("Using grub config dir %s", grubdir)
|
|
|
|
grubCfg, err := g.config.Fs.ReadFile(grubdir)
|
|
if err != nil {
|
|
g.config.Logger.Errorf("Failed reading grub config file: %s", filepath.Join(rootDir, grubConf))
|
|
return err
|
|
}
|
|
|
|
// Create Needed dir under state partition to store the grub.cfg and any needed modules
|
|
err = fsutils.MkdirAll(g.config.Fs, filepath.Join(bootDir, fmt.Sprintf("%s/%s-efi", systemgrub, g.config.Arch)), cnst.DirPerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating grub dir: %s", err)
|
|
}
|
|
|
|
grubConfTarget, err := g.config.Fs.Create(filepath.Join(bootDir, fmt.Sprintf("%s/grub.cfg", systemgrub)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer grubConfTarget.Close()
|
|
|
|
if tty == "" {
|
|
// Get current tty and remove /dev/ from its name
|
|
out, err := g.config.Fs.Readlink("/dev/fd/0")
|
|
tty = strings.TrimPrefix(strings.TrimSpace(out), "/dev/")
|
|
if err != nil {
|
|
g.config.Logger.Warnf("failed to find current tty, leaving it unset")
|
|
tty = ""
|
|
}
|
|
}
|
|
|
|
ttyExists, _ := fsutils.Exists(g.config.Fs, fmt.Sprintf("/dev/%s", tty))
|
|
|
|
if ttyExists && tty != "" && tty != "console" && tty != constants.DefaultTty {
|
|
// We need to add a tty to the grub file
|
|
g.config.Logger.Infof("Adding extra tty (%s) to grub.cfg", tty)
|
|
defConsole := fmt.Sprintf("console=%s", constants.DefaultTty)
|
|
finalContent = strings.Replace(string(grubCfg), defConsole, fmt.Sprintf("%s console=%s", defConsole, tty), -1)
|
|
} else {
|
|
// We don't add anything, just read the file
|
|
finalContent = string(grubCfg)
|
|
}
|
|
|
|
g.config.Logger.Infof("Copying grub contents from %s to %s", grubdir, filepath.Join(bootDir, fmt.Sprintf("%s/grub.cfg", systemgrub)))
|
|
_, err = grubConfTarget.WriteString(finalContent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if efi {
|
|
// Copy required extra modules to boot dir under the state partition
|
|
// otherwise if we insmod it will fail to find them
|
|
// We no longer call grub-install here so the modules are not setup automatically in the state partition
|
|
// as they were before. We now use the bundled grub.efi provided by the shim package
|
|
g.config.Logger.Infof("Generating grub files for efi on %s", target)
|
|
var foundModules bool
|
|
for _, m := range constants.GetGrubModules() {
|
|
err = fsutils.WalkDirFs(g.config.Fs, rootDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.Name() == m && strings.Contains(path, g.config.Arch) {
|
|
fileWriteName := filepath.Join(bootDir, fmt.Sprintf("%s/%s-efi/%s", systemgrub, g.config.Arch, m))
|
|
g.config.Logger.Debugf("Copying %s to %s", path, fileWriteName)
|
|
fileContent, err := g.config.Fs.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading %s: %s", path, err)
|
|
}
|
|
err = g.config.Fs.WriteFile(fileWriteName, fileContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %s", fileWriteName, err)
|
|
}
|
|
foundModules = true
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
if !foundModules {
|
|
return fmt.Errorf("did not find grub modules under %s", rootDir)
|
|
}
|
|
}
|
|
|
|
copyGrubFonts(g.config, rootDir, grubdir, systemgrub)
|
|
|
|
err = fsutils.MkdirAll(g.config.Fs, filepath.Join(cnst.EfiDir, "EFI/boot/"), cnst.DirPerm)
|
|
if err != nil {
|
|
g.config.Logger.Errorf("Error creating dirs: %s", err)
|
|
return err
|
|
}
|
|
|
|
flavor, err := utils.OSRelease("FLAVOR", filepath.Join(cnst.ActiveDir, "etc/kairos-release"))
|
|
if err != nil {
|
|
// Fallback to os-release
|
|
flavor, err = utils.OSRelease("FLAVOR", filepath.Join(cnst.ActiveDir, "os/kairos-release"))
|
|
if err != nil {
|
|
g.config.Logger.Warnf("Failed reading release info from %s and %s: %v", filepath.Join(cnst.ActiveDir, "etc/kairos-release"), filepath.Join(cnst.ActiveDir, "os/kairos-release"), err)
|
|
}
|
|
}
|
|
if flavor == "" {
|
|
// If os-release is gone with our vars, we dont know what flavor are we in, we should know if we are on ubuntu as we need
|
|
// a workaround for the grub efi install
|
|
// So lets try to get the info from the normal keys shipped with the os
|
|
flavorFromId, err := utils.OSRelease("ID", filepath.Join(cnst.ActiveDir, "etc/os-release"))
|
|
if err != nil {
|
|
g.config.Logger.Logger.Err(err).Msg("Getting flavor")
|
|
}
|
|
if strings.Contains(strings.ToLower(flavorFromId), "ubuntu") {
|
|
flavor = "ubuntu"
|
|
}
|
|
}
|
|
g.config.Logger.Debugf("Detected Flavor: %s", flavor)
|
|
// Copy needed files for efi boot
|
|
// This seems like a chore while we could provide a package for those bundled files as they are just a shim and a grub efi
|
|
// BUT this is needed for secureboot
|
|
// The shim contains the signature from microsoft and the shim provider (i.e. upstream distro like fedora, suse, etc)
|
|
// So if we use the shim+grub from a generic package (i.e. ubuntu) it WILL boot with secureboot
|
|
// but when loading the kernel it will fail because the kernel is not signed by the shim provider or grub provider
|
|
// the kernel signature would be from fedora while the shim signature would be from ubuntu
|
|
// This is why if we want to support secureboot we need to copy the shim+grub from the rootfs default paths instead of
|
|
// providing a generic package
|
|
|
|
// Shim is not available in Alpine + rpi
|
|
var model string
|
|
model, err = utils.OSRelease("KAIROS_MODEL", filepath.Join(cnst.ActiveDir, "etc/kairos-release"))
|
|
if err != nil {
|
|
// Fallback into os-release
|
|
model, err = utils.OSRelease("KAIROS_MODEL", filepath.Join(cnst.ActiveDir, "etc/os-release"))
|
|
if err != nil {
|
|
g.config.Logger.Warnf("Failed reading model info from %s and %s: %v", filepath.Join(cnst.ActiveDir, "etc/kairos-release"), filepath.Join(cnst.ActiveDir, "os/kairos-release"), err)
|
|
}
|
|
}
|
|
if strings.Contains(strings.ToLower(flavor), "alpine") && strings.Contains(strings.ToLower(model), "rpi") {
|
|
g.config.Logger.Debug("Running on Alpine+RPI, not copying shim or grub.")
|
|
} else {
|
|
err = g.copyShim()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = g.copyGrub()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Add grub.cfg in EFI that chainloads the grub.cfg in state
|
|
// Notice that we set the config to /grub2/grub.cfg which means the above we need to copy the file from
|
|
// the installation source into that dir
|
|
grubCfgContent := []byte(fmt.Sprintf("search --no-floppy --label --set=root %s\nset prefix=($root)/%s\nconfigfile ($root)/%s/grub.cfg", stateLabel, systemgrub, systemgrub))
|
|
err = g.config.Fs.WriteFile(filepath.Join(cnst.EfiDir, "EFI/boot/grub.cfg"), grubCfgContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %s", filepath.Join(cnst.EfiDir, "EFI/boot/grub.cfg"), err)
|
|
}
|
|
// Ubuntu efi searches for the grub.cfg file under /EFI/ubuntu/grub.cfg while we store it under /boot/grub2/grub.cfg
|
|
// workaround this by copying it there as well
|
|
// read the kairos-release from the rootfs to know if we are creating a ubuntu based iso
|
|
if strings.Contains(strings.ToLower(flavor), "ubuntu") {
|
|
g.config.Logger.Infof("Ubuntu based ISO detected, copying grub.cfg to /EFI/ubuntu/grub.cfg")
|
|
err = fsutils.MkdirAll(g.config.Fs, filepath.Join(cnst.EfiDir, "EFI/ubuntu/"), constants.DirPerm)
|
|
if err != nil {
|
|
g.config.Logger.Errorf("Failed writing grub.cfg: %v", err)
|
|
return err
|
|
}
|
|
err = g.config.Fs.WriteFile(filepath.Join(cnst.EfiDir, "EFI/ubuntu/grub.cfg"), grubCfgContent, constants.FilePerm)
|
|
if err != nil {
|
|
g.config.Logger.Errorf("Failed writing grub.cfg: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findGrubDir will find the grub dir under the dir given if possible by searching for the modinfo.sh
|
|
// And it will return the full dir path where the modinfo.sh is contained
|
|
func findGrubDir(vfs v1.FS, dir string) string {
|
|
var foundPath string
|
|
_ = fsutils.WalkDirFs(vfs, dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.Name() == "modinfo.sh" && strings.Contains(path, "i386-pc") {
|
|
// We found the grub dir, return it
|
|
foundPath = filepath.Dir(path)
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return foundPath
|
|
|
|
}
|
|
|
|
func SetPersistentVariables(grubEnvFile string, vars map[string]string, c *agentConfig.Config) error {
|
|
var b bytes.Buffer
|
|
// Write header
|
|
b.WriteString("# GRUB Environment Block\n")
|
|
|
|
// First we need to read the existing values from the grubenv file if they exist
|
|
finalVars, err := ReadPersistentVariables(grubEnvFile, c)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return fmt.Errorf("error reading existing grubenv file %s: %s", grubEnvFile, err)
|
|
}
|
|
}
|
|
|
|
// Merge the existing vars with the new ones
|
|
// existing vars will be overridden by the new ones from vars if they match
|
|
for key, newValue := range vars {
|
|
if oldValue, exists := finalVars[key]; exists {
|
|
c.Logger.Logger.Warn().Str("key", key).Str("oldValue", oldValue).Str("newValue", newValue).Msg("Overriding existing grubenv variable")
|
|
}
|
|
finalVars[key] = newValue
|
|
}
|
|
|
|
keys := make([]string, 0, len(finalVars))
|
|
|
|
for k := range finalVars {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
if len(finalVars[k]) > 0 {
|
|
b.WriteString(fmt.Sprintf("%s=%s\n", k, finalVars[k]))
|
|
}
|
|
}
|
|
|
|
// https://www.gnu.org/software/grub/manual/grub/html_node/Environment-block.html
|
|
// It has to be exactly 1024bytes, if its not it has to be filled with #
|
|
toBeFilled := 1024 - b.Len()%1024
|
|
for i := 0; i < toBeFilled; i++ {
|
|
b.WriteByte('#')
|
|
}
|
|
return c.Fs.WriteFile(grubEnvFile, b.Bytes(), cnst.FilePerm)
|
|
}
|
|
|
|
// copyGrubFonts will try to finds and copy the needed grub fonts into the system
|
|
// rootdir is the dir where to search for the fonts
|
|
// bootdir is the base dir where they will be copied
|
|
func copyGrubFonts(cfg *agentConfig.Config, rootDir, bootDir, systemgrub string) {
|
|
for _, m := range constants.GetGrubFonts() {
|
|
var foundFont bool
|
|
_ = fsutils.WalkDirFs(cfg.Fs, rootDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.Name() == m && strings.Contains(path, cfg.Arch) {
|
|
fileWriteName := filepath.Join(bootDir, fmt.Sprintf("%s/%s-efi/fonts/%s", systemgrub, cfg.Arch, m))
|
|
cfg.Logger.Debugf("Copying %s to %s", path, fileWriteName)
|
|
fileContent, err := cfg.Fs.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading %s: %s", path, err)
|
|
}
|
|
err = cfg.Fs.WriteFile(fileWriteName, fileContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %s", fileWriteName, err)
|
|
}
|
|
foundFont = true
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
if !foundFont {
|
|
// Not a real error as to fail install but a big warning
|
|
cfg.Logger.Warnf("did not find grub font %s under %s", m, rootDir)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g Grub) copyShim() error {
|
|
shimFiles := utils.GetEfiShimFiles(g.config.Arch)
|
|
shimDone := false
|
|
for _, f := range shimFiles {
|
|
_, err := g.config.Fs.Stat(filepath.Join(cnst.ActiveDir, f))
|
|
if err != nil {
|
|
g.config.Logger.Debugf("skip copying %s: not found", filepath.Join(cnst.ActiveDir, f))
|
|
continue
|
|
}
|
|
_, name := filepath.Split(f)
|
|
// remove the .signed suffix if present
|
|
name = strings.TrimSuffix(name, ".signed")
|
|
fileWriteName := filepath.Join(cnst.EfiDir, fmt.Sprintf("EFI/boot/%s", name))
|
|
g.config.Logger.Debugf("Copying %s to %s", f, fileWriteName)
|
|
|
|
// Try to find the paths give until we succeed
|
|
fileContent, err := g.config.Fs.ReadFile(filepath.Join(cnst.ActiveDir, f))
|
|
|
|
if err != nil {
|
|
g.config.Logger.Warnf("error reading %s: %s", filepath.Join(cnst.ActiveDir, f), err)
|
|
continue
|
|
}
|
|
err = g.config.Fs.WriteFile(fileWriteName, fileContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %s", fileWriteName, err)
|
|
}
|
|
shimDone = true
|
|
|
|
// Copy the shim content to the fallback name so the system boots from fallback. This means that we do not create
|
|
// any bootloader entries, so our recent installation has the lower priority if something else is on the bootloader
|
|
writeShim := cnst.GetFallBackEfi(g.config.Arch)
|
|
err = g.config.Fs.WriteFile(filepath.Join(cnst.EfiDir, "EFI/boot/", writeShim), fileContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("could not write shim file %s at dir %s", writeShim, cnst.EfiDir)
|
|
}
|
|
break
|
|
}
|
|
if !shimDone {
|
|
g.config.Logger.Debugf("List of shim files searched for: %s", shimFiles)
|
|
return fmt.Errorf("could not find any shim file to copy")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g Grub) copyGrub() error {
|
|
grubFiles := utils.GetEfiGrubFiles(g.config.Arch)
|
|
grubDone := false
|
|
for _, f := range grubFiles {
|
|
_, err := g.config.Fs.Stat(filepath.Join(constants.ActiveDir, f))
|
|
if err != nil {
|
|
g.config.Logger.Debugf("skip copying %s: not found", filepath.Join(constants.ActiveDir, f))
|
|
continue
|
|
}
|
|
_, name := filepath.Split(f)
|
|
// remove the .signed suffix if present
|
|
name = strings.TrimSuffix(name, ".signed")
|
|
fileWriteName := filepath.Join(cnst.EfiDir, fmt.Sprintf("EFI/boot/%s", name))
|
|
g.config.Logger.Debugf("Copying %s to %s", f, fileWriteName)
|
|
|
|
// Try to find the paths give until we succeed
|
|
fileContent, err := g.config.Fs.ReadFile(filepath.Join(cnst.ActiveDir, f))
|
|
|
|
if err != nil {
|
|
g.config.Logger.Warnf("error reading %s: %s", filepath.Join(cnst.ActiveDir, f), err)
|
|
continue
|
|
}
|
|
err = g.config.Fs.WriteFile(fileWriteName, fileContent, cnst.FilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %s", fileWriteName, err)
|
|
}
|
|
grubDone = true
|
|
break
|
|
}
|
|
if !grubDone {
|
|
g.config.Logger.Debugf("List of grub files searched for: %s", grubFiles)
|
|
return fmt.Errorf("could not find any grub efi file to copy")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadPersistentVariables will read a grub env file and parse the values
|
|
func ReadPersistentVariables(grubEnvFile string, c *agentConfig.Config) (map[string]string, error) {
|
|
vars := make(map[string]string)
|
|
f, err := c.Fs.ReadFile(grubEnvFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, a := range strings.Split(string(f), "\n") {
|
|
// comment or fillup, so skip
|
|
if strings.HasPrefix(a, "#") {
|
|
continue
|
|
}
|
|
splitted := strings.Split(a, "=")
|
|
if len(splitted) == 2 {
|
|
vars[splitted[0]] = splitted[1]
|
|
} else {
|
|
return nil, fmt.Errorf("invalid format for %s", a)
|
|
}
|
|
}
|
|
return vars, nil
|
|
}
|