From 7842ad80594d2182eab06381af02ad4075a70439 Mon Sep 17 00:00:00 2001 From: Itxaka Date: Fri, 23 May 2025 11:48:26 +0200 Subject: [PATCH] Pxe uki (#791) --- internal/agent/install.go | 37 ++++++++--- pkg/constants/constants.go | 6 ++ pkg/utils/common.go | 122 ++++++++++++++++++++++++++++++++++--- pkg/utils/loop/loopback.go | 11 +++- 4 files changed, 159 insertions(+), 17 deletions(-) diff --git a/internal/agent/install.go b/internal/agent/install.go index adfa6c3..b199a9a 100644 --- a/internal/agent/install.go +++ b/internal/agent/install.go @@ -13,23 +13,22 @@ import ( "syscall" "time" - "github.com/kairos-io/kairos-agent/v2/pkg/uki" - internalutils "github.com/kairos-io/kairos-agent/v2/pkg/utils" - - fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" - "github.com/sanity-io/litter" - qr "github.com/kairos-io/go-nodepair/qrcode" "github.com/kairos-io/kairos-agent/v2/internal/bus" "github.com/kairos-io/kairos-agent/v2/internal/cmd" "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/constants" + "github.com/kairos-io/kairos-agent/v2/pkg/uki" + internalutils "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/kairos-io/kairos-sdk/collector" "github.com/kairos-io/kairos-sdk/machine" "github.com/kairos-io/kairos-sdk/utils" "github.com/mudler/go-pluggable" "github.com/pterm/pterm" + "github.com/sanity-io/litter" ) func displayInfo(agentConfig *Config) { @@ -236,6 +235,13 @@ func RunInstall(c *config.Config) error { // runInstallUki runs the UKI path install func runInstallUki(c *config.Config) error { + // Check if we are running in PXE + err := internalutils.SetPXEEnv(c) + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("Error setting PXE environment") + return err + } + // Load the spec from the config installSpec, err := config.ReadUkiInstallSpecFromConfig(c) if err != nil { @@ -258,7 +264,24 @@ func runInstallUki(c *config.Config) error { c.CloudInitPaths = append(c.CloudInitPaths, installSpec.CloudInit...) installAction := uki.NewInstallAction(c, installSpec) - return installAction.Run() + err = installAction.Run() + + if err == nil && utils.Exists(constants.PXEVarFile) { + // TODO: do we fail here? + err = internalutils.RemoveEfivarPXE(c.Logger) + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("Error removing PXE Efivar") + return err + } + // Now remove the boot entry + // TODO: Do we fail here? + err = internalutils.RemoveBootEntry("kairos", c.Logger) + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("Error removing PXE boot entry") + return err + } + } + return err } // runInstall runs the non-UKI path install diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index b2f5340..94c1832 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -50,6 +50,7 @@ const ( LinuxImgFs = "ext2" SquashFs = "squashfs" EfiFs = "vfat" + IsoFS = "iso9660" EfiSize = uint(64) OEMSize = uint(64) PersistentSize = uint(0) @@ -130,6 +131,11 @@ const ( UpgradeRecoveryNoSourceError = "could not find a proper source for the recovery upgrade.\nThis can be configured in the cloud config files under the 'upgrade.recovery-system.uri' key or via cmdline using the '--source' flag" MultipleEntriesAssessmentError = "multiple boot entries found for %s" NoBootAssessmentWarning = "No boot assessment found in current boot entry config file" + + // PXEVarFile is the path to the PXE boot entry file + PXEVarFile = "/sys/firmware/efi/efivars/PXEBoot-3c909ff1-80a9-5970-94f1-fefb255c88bd" + PXEIsoFile = "/tmp/pxe-source.iso" + PXEEfiBootFile = "efiboot.img" ) func UkiDefaultMenuEntries() []string { diff --git a/pkg/utils/common.go b/pkg/utils/common.go index d727bd2..11ef9c1 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -21,6 +21,8 @@ import ( "crypto/sha256" "errors" "fmt" + "github.com/foxboron/go-uefi/efi/attr" + "github.com/kairos-io/kairos-agent/v2/pkg/utils/loop" "io" "io/fs" random "math/rand" @@ -33,19 +35,18 @@ import ( "strings" "time" - sdkTypes "github.com/kairos-io/kairos-sdk/types" - - "github.com/kairos-io/kairos-sdk/state" - "github.com/kairos-io/kairos-sdk/types" - - agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" - fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" - "github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" - "github.com/distribution/reference" + "github.com/foxboron/go-uefi/efi/attributes" + "github.com/foxboron/go-uefi/efivarfs" "github.com/joho/godotenv" + agentConfig "github.com/kairos-io/kairos-agent/v2/pkg/config" cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" + fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + "github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" + "github.com/kairos-io/kairos-sdk/state" + "github.com/kairos-io/kairos-sdk/types" + sdkTypes "github.com/kairos-io/kairos-sdk/types" "github.com/twpayne/go-vfs/v5" ) @@ -707,3 +708,106 @@ func ReadAssessmentFromEntry(fs v1.FS, entry string, logger sdkTypes.KairosLogge } return re.FindStringSubmatch(currentfile[0])[1], nil } + +// ReadEfivar reads the content of an efivar file, skipping the first 4 bytes (attributes). +func ReadEfivar(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + // Skip the first 4 bytes (attributes) + attr := make([]byte, 4) + _, err = io.ReadFull(f, attr) + if err != nil { + return nil, err + } + + content, err := io.ReadAll(f) + if err != nil { + return nil, err + } + return content, nil +} + +func RemoveBootEntry(entryName string, l types.KairosLogger) error { + // Remove any boot entries that match kairos + efifs := efivarfs.NewFS().CheckImmutable().UnsetImmutable().Open() + for _, entry := range efifs.GetBootOrder() { + e, err := efifs.GetBootEntry(entry) + if err != nil { + return err + } + // TODO: maybe contains? so we match any variations that migth appear in the future? + if strings.ToLower(e.Description) == strings.ToLower(entryName) { + fileName := fmt.Sprintf("/sys/firmware/efi/efivars/%s-%s", entry, attributes.EFI_GLOBAL_VARIABLE.Format()) + l.Logger.Debug().Str("entry", e.Description).Str("filename", fileName).Msg("Removing boot entry") + err = os.Remove(fileName) + if err != nil { + l.Logger.Err(err).Str("entry", e.Description).Str("filename", fileName).Msg("Error removing boot entry") + return err + } + } + } + return nil +} + +func RemoveEfivarPXE(l types.KairosLogger) error { + // Disable immutability + err := attr.UnsetImmutable(cnst.PXEVarFile) + if err != nil { + l.Logger.Err(err).Str("file", cnst.PXEVarFile).Msg("Error unsetting immutable attribute") + return err + } + return os.Remove(cnst.PXEVarFile) +} + +func SetPXEEnv(c *agentConfig.Config) error { + efivar, err := ReadEfivar(cnst.PXEVarFile) + // We dont care if we fail to read it, that means its not there + if err == nil { + c.Logger.Logger.Info().Str("iso", string(efivar)).Msg("PXE boot detected, downloading and mounting the iso locally") + err = c.Client.GetURL(c.Logger, string(efivar), cnst.PXEIsoFile) + if err != nil { + return err + } + + isoLoop, err := loop.LoopRO(&v1.Image{File: cnst.PXEIsoFile}, c) + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("Error creating loop device for iso image") + return err + } + defer loop.Unloop(isoLoop, c) + c.Logger.Logger.Debug().Str("iso", isoLoop).Msg("Mounted iso loop device") + + // Mount the iso under /run/initramfs/live + err = c.Mounter.Mount(isoLoop, cnst.UkiCdromSource, cnst.IsoFS, nil) + if err != nil { + c.Logger.Errorf("Error mounting iso: %s", err.Error()) + return err + } + c.Logger.Infof("Mounted iso under %s", cnst.UkiCdromSource) + defer c.Mounter.Unmount(cnst.UkiCdromSource) + + // Now mount the efi image inside the iso + efiLoop, err := loop.LoopRO(&v1.Image{File: filepath.Join(cnst.UkiCdromSource, cnst.PXEEfiBootFile)}, c) + if err != nil { + c.Logger.Logger.Error().Err(err).Msg("Error creating loop device for efi image") + return err + } + defer loop.Unloop(efiLoop, c) + c.Logger.Logger.Debug().Str("efi", efiLoop).Msg("Mounted efi loop device") + + // Mount the efi image under /run/rootfsbase which is the same as to other boot paths mount it at + err = c.Mounter.Mount(efiLoop, cnst.IsoBaseTree, cnst.EfiFs, nil) + if err != nil { + c.Logger.Errorf("Error mounting iso: %s", err.Error()) + return err + } + c.Logger.Infof("Mounted Efi source under %s", cnst.IsoBaseTree) + defer c.Mounter.Unmount(cnst.IsoBaseTree) + // Now the system should have the same paths and sources as the normal install from usb/cdrom + } + return nil +} diff --git a/pkg/utils/loop/loopback.go b/pkg/utils/loop/loopback.go index 74673f0..19987bc 100644 --- a/pkg/utils/loop/loopback.go +++ b/pkg/utils/loop/loopback.go @@ -23,6 +23,15 @@ func errnoIsErr(err error) error { // Loop will setup a /dev/loopX device linked to the image file by using syscalls directly to set it func Loop(img *v1.Image, cfg *config.Config) (loopDevice string, err error) { + return loop(img, cfg, os.O_RDWR) +} + +// LoopRO will setup a /dev/loopX device linked to the image file by using syscalls directly to set it. Will open it RO +func LoopRO(img *v1.Image, cfg *config.Config) (loopDevice string, err error) { + return loop(img, cfg, os.O_RDONLY) +} + +func loop(img *v1.Image, cfg *config.Config, openPerm int) (loopDevice string, err error) { log := cfg.Logger log.Debugf("Opening loop control device") fd, err := cfg.Fs.OpenFile("/dev/loop-control", os.O_RDONLY, 0o644) @@ -47,7 +56,7 @@ func Loop(img *v1.Image, cfg *config.Config) (loopDevice string, err error) { return loopDevice, err } log.Logger.Debug().Str("image", img.File).Msg("Opening img file") - imageFile, err := cfg.Fs.OpenFile(img.File, os.O_RDWR, os.ModePerm) + imageFile, err := cfg.Fs.OpenFile(img.File, openPerm, os.ModePerm) if err != nil { log.Error("failed to open image file") return loopDevice, err