2023-05-05 16:43:21 +00:00
|
|
|
/*
|
|
|
|
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 (
|
|
|
|
"fmt"
|
2023-07-25 13:21:34 +00:00
|
|
|
agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
|
|
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
2024-01-11 10:24:43 +00:00
|
|
|
"github.com/kairos-io/kairos-sdk/utils"
|
2023-05-05 16:43:21 +00:00
|
|
|
"io/fs"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2023-07-10 12:39:48 +00:00
|
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
|
|
|
cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
2023-05-05 16:43:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Grub is the struct that will allow us to install grub to the target device
|
|
|
|
type Grub struct {
|
2023-07-25 13:21:34 +00:00
|
|
|
config *agentConfig.Config
|
2023-05-05 16:43:21 +00:00
|
|
|
}
|
|
|
|
|
2023-07-25 13:21:34 +00:00
|
|
|
func NewGrub(config *agentConfig.Config) *Grub {
|
2023-05-05 16:43:21 +00:00
|
|
|
g := &Grub{
|
|
|
|
config: config,
|
|
|
|
}
|
|
|
|
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// Install installs grub into the device, copy the config file and add any extra TTY to grub
|
|
|
|
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..")
|
|
|
|
|
|
|
|
grubargs = append(
|
|
|
|
grubargs,
|
|
|
|
fmt.Sprintf("--root-directory=%s", rootDir),
|
|
|
|
fmt.Sprintf("--boot-directory=%s", bootDir),
|
|
|
|
"--target=i386-pc",
|
|
|
|
target,
|
|
|
|
)
|
|
|
|
|
|
|
|
g.config.Logger.Debugf("Running grub with the following args: %s", grubargs)
|
|
|
|
out, err := g.config.Runner.Run(FindCommand("grub2-install", []string{"grub2-install", "grub-install"}), 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
|
2023-07-25 13:21:34 +00:00
|
|
|
if ok, _ := fsutils.IsDir(g.config.Fs, filepath.Join(bootDir, "grub")); ok {
|
2023-05-05 16:43:21 +00:00
|
|
|
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
|
2023-07-25 13:21:34 +00:00
|
|
|
err = fsutils.MkdirAll(g.config.Fs, filepath.Join(bootDir, fmt.Sprintf("%s/%s-efi", systemgrub, g.config.Arch)), cnst.DirPerm)
|
2023-05-05 16:43:21 +00:00
|
|
|
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.Runner.Run("tty")
|
|
|
|
tty = strings.TrimPrefix(strings.TrimSpace(string(out)), "/dev/")
|
|
|
|
if err != nil {
|
|
|
|
g.config.Logger.Warnf("failed to find current tty, leaving it unset")
|
|
|
|
tty = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-25 13:21:34 +00:00
|
|
|
ttyExists, _ := fsutils.Exists(g.config.Fs, fmt.Sprintf("/dev/%s", tty))
|
2023-05-05 16:43:21 +00:00
|
|
|
|
|
|
|
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
|
2023-09-13 09:07:28 +00:00
|
|
|
for _, m := range constants.GetGrubModules() {
|
2023-07-25 13:21:34 +00:00
|
|
|
err = fsutils.WalkDirFs(g.config.Fs, rootDir, func(path string, d fs.DirEntry, err error) error {
|
2023-05-05 16:43:21 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-13 09:07:28 +00:00
|
|
|
copyGrubFonts(g.config, rootDir, grubdir, systemgrub)
|
|
|
|
|
2023-07-25 13:21:34 +00:00
|
|
|
err = fsutils.MkdirAll(g.config.Fs, filepath.Join(cnst.EfiDir, "EFI/boot/"), cnst.DirPerm)
|
2023-05-05 16:43:21 +00:00
|
|
|
if err != nil {
|
|
|
|
g.config.Logger.Errorf("Error creating dirs: %s", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy needed files for efi boot
|
2024-01-11 10:24:43 +00:00
|
|
|
// 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
|
|
|
|
err = g.copyShim()
|
2023-05-05 16:43:21 +00:00
|
|
|
if err != nil {
|
2024-01-11 10:24:43 +00:00
|
|
|
return err
|
2023-05-05 16:43:21 +00:00
|
|
|
}
|
2024-01-11 10:24:43 +00:00
|
|
|
err = g.copyGrub()
|
2023-05-05 16:43:21 +00:00
|
|
|
if err != nil {
|
2024-01-11 10:24:43 +00:00
|
|
|
return err
|
2023-05-05 16:43:21 +00:00
|
|
|
}
|
|
|
|
// Add grub.cfg in EFI that chainloads the grub.cfg in recovery
|
|
|
|
// 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)
|
|
|
|
}
|
2024-01-15 14:15:05 +00:00
|
|
|
// 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 os-release from the rootfs to know if we are creating a ubuntu based iso
|
|
|
|
flavor, err := utils.OSRelease("FLAVOR", filepath.Join(cnst.ActiveDir, "etc/os-release"))
|
|
|
|
if err != nil {
|
|
|
|
g.config.Logger.Warnf("Failed reading os-release from %s: %v", filepath.Join(cnst.ActiveDir, "etc/os-release"), err)
|
|
|
|
}
|
|
|
|
g.config.Logger.Infof("Detected Flavor: %s", flavor)
|
|
|
|
if err == nil && 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-05 16:43:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sets the given key value pairs into as grub variables into the given file
|
|
|
|
func (g Grub) SetPersistentVariables(grubEnvFile string, vars map[string]string) error {
|
|
|
|
for key, value := range vars {
|
|
|
|
g.config.Logger.Debugf("Running grub2-editenv with params: %s set %s=%s", grubEnvFile, key, value)
|
|
|
|
out, err := g.config.Runner.Run(FindCommand("grub2-editenv", []string{"grub2-editenv", "grub-editenv"}), grubEnvFile, "set", fmt.Sprintf("%s=%s", key, value))
|
|
|
|
if err != nil {
|
|
|
|
g.config.Logger.Errorf(fmt.Sprintf("Failed setting grub variables: %s", out))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-09-13 09:07:28 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-11 10:24:43 +00:00
|
|
|
|
|
|
|
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
|
2024-01-11 10:51:46 +00:00
|
|
|
name = strings.TrimSuffix(name, ".signed")
|
2024-01-11 10:24:43 +00:00
|
|
|
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
|
2024-01-11 10:51:46 +00:00
|
|
|
name = strings.TrimSuffix(name, ".signed")
|
2024-01-11 10:24:43 +00:00
|
|
|
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
|
|
|
|
}
|