mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-04-27 19:28:59 +00:00
261 lines
8.9 KiB
Go
261 lines
8.9 KiB
Go
package uki
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/action"
|
|
|
|
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"
|
|
sdkutils "github.com/kairos-io/kairos-sdk/utils"
|
|
)
|
|
|
|
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
|
|
if err = utils.RunStage(i.cfg, "kairos-uki-install.pre"); err != nil {
|
|
i.cfg.Logger.Errorf("running kairos-uki-install.pre stage: %s", err.Error())
|
|
}
|
|
if err = events.RunHookScript("/usr/bin/kairos-agent.uki.install.pre.hook"); err != nil {
|
|
i.cfg.Logger.Errorf("running kairos-uki-install.pre hook script: %s", err.Error())
|
|
}
|
|
|
|
if i.spec.NoFormat {
|
|
i.cfg.Logger.Infof("NoFormat is true, skipping format and partitioning")
|
|
if i.spec.Target == "" || i.spec.Target == "auto" {
|
|
// This needs to run after the pre-install stage to give the user the
|
|
// opportunity to prepare the target disk in the pre-install stage.
|
|
device, err := config.DetectPreConfiguredDevice(i.cfg.Logger)
|
|
if err != nil {
|
|
return fmt.Errorf("no target device specified and no device found: %s", err)
|
|
}
|
|
i.cfg.Logger.Infof("No target device specified, using pre-configured device: %s", device)
|
|
i.spec.Target = device
|
|
}
|
|
} else {
|
|
// Deactivate any active volume on target
|
|
err = e.DeactivateDevices()
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("deactivating devices: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Partition device
|
|
err = e.PartitionAndFormatDevice(i.spec)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("partitioning and formating devices: %s", err.Error())
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = e.MountPartitions(i.spec.GetPartitions().PartitionsByMountPoint(false))
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("mounting partitions: %s", err.Error())
|
|
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 {
|
|
i.cfg.Logger.Errorf("running before install hook: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Check if we should fail the installation by checking the sentinel file FailInstallationFileSentinel
|
|
if toFail, err := utils.CheckFailedInstallation(constants.FailInstallationFileSentinel); toFail {
|
|
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 {
|
|
i.cfg.Logger.Errorf("copying cloud config: %s", err.Error())
|
|
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 {
|
|
i.cfg.Logger.Errorf("creating efi directories: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// TODO: Check if the size of the files we are going to copy, will fit in the
|
|
// partition. If not stop here.
|
|
|
|
// Copy the efi file into the proper dir
|
|
_, err = e.DumpSource(i.spec.Partitions.EFI.MountPoint, i.spec.Active.Source)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("dumping source: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// Remove entries
|
|
// Read all confs
|
|
i.cfg.Logger.Debugf("Parsing efi partition files (skip SkipEntries, replace placeholders etc)")
|
|
err = fsutils.WalkDirFs(i.cfg.Fs, filepath.Join(i.spec.Partitions.EFI.MountPoint), func(path string, info os.DirEntry, err error) error {
|
|
filename := info.Name()
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("Error walking path: %s, %s", filename, err.Error())
|
|
return err
|
|
}
|
|
|
|
i.cfg.Logger.Debugf("Checking file %s", path)
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if filepath.Ext(filename) == ".conf" {
|
|
// Extract the values
|
|
conf, err := sdkutils.SystemdBootConfReader(path)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("Error reading conf file to extract values %s: %s", path, err)
|
|
return err
|
|
}
|
|
if len(conf["cmdline"]) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Check if the cmdline matches any of the entries in the skip list
|
|
skip := false
|
|
for _, entry := range i.spec.SkipEntries {
|
|
if strings.Contains(conf["cmdline"], entry) {
|
|
i.cfg.Logger.Debugf("Found match for %s in %s", entry, path)
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
if skip {
|
|
return i.SkipEntry(path, conf)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("error happened: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
for _, role := range []string{"active", "passive", "recovery", "statereset"} {
|
|
if err = copyArtifactSetRole(i.cfg.Fs, i.spec.Partitions.EFI.MountPoint, UnassignedArtifactRole, role, i.cfg.Logger); err != nil {
|
|
i.cfg.Logger.Errorf("installing the new artifact set as %s: %s", role, err.Error())
|
|
return fmt.Errorf("installing the new artifact set as %s: %w", role, err)
|
|
}
|
|
}
|
|
|
|
loaderConfPath := filepath.Join(i.spec.Partitions.EFI.MountPoint, "loader", "loader.conf")
|
|
if err = replaceRoleInKey(loaderConfPath, "default", UnassignedArtifactRole, "active", i.cfg.Logger); err != nil {
|
|
i.cfg.Logger.Errorf("replacing role in key %s: %s", "default", err.Error())
|
|
return err
|
|
}
|
|
|
|
if err = removeArtifactSetWithRole(i.cfg.Fs, i.spec.Partitions.EFI.MountPoint, UnassignedArtifactRole); err != nil {
|
|
i.cfg.Logger.Errorf("removing artifact set with role %s: %s", UnassignedArtifactRole, err.Error())
|
|
return fmt.Errorf("removing artifact set with role %s: %w", UnassignedArtifactRole, err)
|
|
}
|
|
|
|
// add sort key to all files
|
|
err = AddSystemdConfSortKey(i.cfg.Fs, i.spec.Partitions.EFI.MountPoint, i.cfg.Logger)
|
|
if err != nil {
|
|
i.cfg.Logger.Warnf("adding sort key: %s", err.Error())
|
|
}
|
|
|
|
// Add boot assessment to files by appending +3 to the name
|
|
err = utils.AddBootAssessment(i.cfg.Fs, i.spec.Partitions.EFI.MountPoint, i.cfg.Logger)
|
|
if err != nil {
|
|
i.cfg.Logger.Warnf("adding boot assesment: %s", err.Error())
|
|
}
|
|
|
|
// SelectBootEntry sets the default boot entry to the selected entry
|
|
err = action.SelectBootEntry(i.cfg, "cos")
|
|
if err != nil {
|
|
i.cfg.Logger.Warnf("selecting active boot entry: %s", err.Error())
|
|
}
|
|
|
|
err = hook.Run(*i.cfg, i.spec, hook.PostInstall...)
|
|
if err != nil {
|
|
i.cfg.Logger.Errorf("running uki encryption hooks: %s", err.Error())
|
|
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 {
|
|
i.cfg.Logger.Errorf("running after install hook: %s", err.Error())
|
|
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?
|
|
if err = utils.RunStage(i.cfg, "kairos-uki-install.after"); err != nil {
|
|
i.cfg.Logger.Errorf("running kairos-uki-install.after stage: %s", err.Error())
|
|
}
|
|
|
|
if err = events.RunHookScript("/usr/bin/kairos-agent.uki.install.after.hook"); err != nil {
|
|
i.cfg.Logger.Errorf("running kairos-uki-install.after hook script: %s", err.Error())
|
|
}
|
|
|
|
return hook.Run(*i.cfg, i.spec, hook.FinishUKIInstall...)
|
|
}
|
|
|
|
func (i *InstallAction) SkipEntry(path string, conf map[string]string) (err error) {
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|