kairos-agent/pkg/uki/install.go

218 lines
7.2 KiB
Go

package uki
import (
"os"
"path/filepath"
"strings"
hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks"
"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/elemental"
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"
events "github.com/kairos-io/kairos-sdk/bus"
"github.com/sanity-io/litter"
)
type InstallAction struct {
cfg *config.Config
spec *v1.InstallUkiSpec
}
func NewInstallAction(cfg *config.Config, spec *v1.InstallUkiSpec) *InstallAction {
return &InstallAction{cfg: cfg, spec: spec}
}
func (i *InstallAction) Run() (err error) {
e := elemental.NewElemental(i.cfg)
cleanup := utils.NewCleanStack()
defer func() { err = cleanup.Cleanup(err) }()
// Run pre-install stage
_ = utils.RunStage(i.cfg, "kairos-uki-install.pre")
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.install.pre.hook")
// Get source (from spec?)
// If source is empty then we need to find the media we booted from....to get the efi files...
// cdrom is kind fo easy...
// we set the label EFI_ISO_BOOT so we look for that and then mount the image inside...
// TODO: Extract this to a different functions or something. Maybe PrepareUKISource or something
_ = fsutils.MkdirAll(i.cfg.Fs, constants.UkiCdromSource, os.ModeDir|os.ModePerm)
_ = fsutils.MkdirAll(i.cfg.Fs, constants.UkiSource, os.ModeDir|os.ModePerm)
cdRom := &v1.Partition{
FilesystemLabel: "UKI_ISO_INSTALL", // TODO: Hardcoded on ISO creation
FS: "iso9660",
Path: "/dev/disk/by-label/UKI_ISO_INSTALL",
MountPoint: constants.UkiCdromSource,
}
err = e.MountPartition(cdRom)
if err != nil {
return err
}
cleanup.Push(func() error {
return e.UnmountPartition(cdRom)
})
// TODO: hardcoded
image := &v1.Image{
File: "/run/install/cdrom/efiboot.img", // TODO: Hardcoded on ISO creation
Label: "UKI_SOURCE", // Made up, only for logging
MountPoint: constants.UkiSource,
}
err = e.MountImage(image)
if err != nil {
return err
}
cleanup.Push(func() error {
return e.UnmountImage(image)
})
source := v1.NewDirSrc(constants.UkiSource)
// Get the actual source size to calculate the image size and partitions size
size, err := config.GetSourceSize(i.cfg, source)
if err != nil {
i.cfg.Logger.Warnf("Failed to infer size for images, leaving it as default size (%sMb): %s", i.spec.Partitions.EFI.Size, err.Error())
} else {
// Only override if the calculated size is bigger than the default size, otherwise stay with 15Gb minimum
if uint(size*3) > i.spec.Partitions.EFI.Size {
i.spec.Partitions.EFI.Size = uint(size * 3)
}
}
i.cfg.Logger.Infof("Setting image size to %dMb", i.spec.Partitions.EFI.Size)
// Create EFI partition (fat32), we already create the efi partition on normal efi install,we can reuse that?
// Create COS_OEM/COS_PERSISTANT if set (optional)
// I guess we need to set sensible default values here for sizes? oem -> 64Mb as usual but if no persistent then EFI max size?
// if persistent then EFI = source size * 2 (or maybe 3 times! so we can upgrade!) and then persistent the rest of the disk?
// Deactivate any active volume on target
err = e.DeactivateDevices()
if err != nil {
return err
}
// Partition device
err = e.PartitionAndFormatDevice(i.spec)
if err != nil {
return err
}
err = e.MountPartitions(i.spec.GetPartitions().PartitionsByMountPoint(false))
if err != nil {
return err
}
cleanup.Push(func() error {
return e.UnmountPartitions(i.spec.GetPartitions().PartitionsByMountPoint(true))
})
// Before install hook happens after partitioning but before the image OS is applied (this is for compatibility with normal install, so users can reuse their configs)
err = Hook(i.cfg, constants.BeforeInstallHook)
if err != nil {
return err
}
// Store cloud-config in TPM or copy it to COS_OEM?
// Copy cloud-init if any
err = e.CopyCloudConfig(i.spec.CloudInit)
if err != nil {
return err
}
// Create dir structure
// - /EFI/Kairos/ -> Store our older efi images ?
// - /EFI/BOOT/ -> Default fallback dir (efi search for bootaa64.efi or bootx64.efi if no entries in the boot manager)
err = fsutils.MkdirAll(i.cfg.Fs, filepath.Join(constants.EfiDir, "EFI", "BOOT"), constants.DirPerm)
if err != nil {
return err
}
// Copy the efi file into the proper dir
_, err = e.DumpSource(i.spec.Partitions.EFI.MountPoint, source)
if err != nil {
return err
}
// Remove entries
// Read all confs
i.cfg.Logger.Debugf("Checking for entries to remove")
err = fsutils.WalkDirFs(i.cfg.Fs, filepath.Join(i.spec.Partitions.EFI.MountPoint, "loader/entries/"), func(path string, info os.DirEntry, err error) error {
i.cfg.Logger.Debugf("Checking file %s", path)
if info.IsDir() {
return nil
}
if filepath.Ext(info.Name()) != ".conf" {
return nil
}
// Extract the values
conf, err := utils.SystemdBootConfReader(path)
if err != nil {
i.cfg.Logger.Errorf("Error reading conf file %s: %s", path, err)
return err
}
i.cfg.Logger.Debugf("Conf file %s has values %v", path, litter.Sdump(conf))
if len(conf["cmdline"]) == 0 {
return nil
}
// Check if the cmdline matches any of the entries in the skip list
for _, entry := range i.spec.SkipEntries {
// Match the cmdline key against the entry
if strings.Contains(conf["cmdline"], entry) {
i.cfg.Logger.Debugf("Found match for %s in %s", entry, path)
// If match, get the efi file and remove it
if conf["efi"] != "" {
i.cfg.Logger.Debugf("Removing efi file %s", conf["efi"])
// First remove the efi file
err = i.cfg.Fs.Remove(filepath.Join(i.spec.Partitions.EFI.MountPoint, conf["efi"]))
if err != nil {
i.cfg.Logger.Errorf("Error removing efi file %s: %s", conf["efi"], err)
return err
}
// Then remove the conf file
i.cfg.Logger.Debugf("Removing conf file %s", path)
err = i.cfg.Fs.Remove(path)
if err != nil {
i.cfg.Logger.Errorf("Error removing conf file %s: %s", path, err)
return err
}
// Do not continue checking the conf file, we already done all we needed
}
}
}
return err
})
if err != nil {
return err
}
// after install hook happens after install (this is for compatibility with normal install, so users can reuse their configs)
err = Hook(i.cfg, constants.AfterInstallHook)
if err != nil {
return err
}
// Remove all boot manager entries?
// Create boot manager entry
// Set default entry to the one we just created
// Probably copy efi utils, like the Mokmanager and even the shim or grub efi to help with troubleshooting?
_ = utils.RunStage(i.cfg, "kairos-uki-install.after")
_ = events.RunHookScript("/usr/bin/kairos-agent.uki.install.after.hook") //nolint:errcheck
return hook.Run(*i.cfg, i.spec, hook.AfterUkiInstall...)
}
// Hook is RunStage wrapper that only adds logic to ignore errors
// in case v1.Config.Strict is set to false
func Hook(config *config.Config, hook string) error {
config.Logger.Infof("Running %s hook", hook)
err := utils.RunStage(config, hook)
if !config.Strict {
err = nil
}
return err
}