361 lines
9.4 KiB
Go
361 lines
9.4 KiB
Go
package action
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kairos-io/enki/pkg/constants"
|
|
"github.com/kairos-io/enki/pkg/utils"
|
|
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
|
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
|
sdk "github.com/kairos-io/kairos-sdk/utils"
|
|
)
|
|
|
|
type BuildISOAction struct {
|
|
cfg *v1.BuildConfig
|
|
spec *v1.LiveISO
|
|
e *elemental.Elemental
|
|
}
|
|
|
|
type BuildISOActionOption func(a *BuildISOAction)
|
|
|
|
func NewBuildISOAction(cfg *v1.BuildConfig, spec *v1.LiveISO, opts ...BuildISOActionOption) *BuildISOAction {
|
|
b := &BuildISOAction{
|
|
cfg: cfg,
|
|
e: elemental.NewElemental(&cfg.Config),
|
|
spec: spec,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(b)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ISORun will install the system from a given configuration
|
|
func (b *BuildISOAction) ISORun() (err error) {
|
|
cleanup := sdk.NewCleanStack()
|
|
defer func() { err = cleanup.Cleanup(err) }()
|
|
|
|
isoTmpDir, err := utils.TempDir(b.cfg.Fs, "", "enki-iso")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cleanup.Push(func() error { return b.cfg.Fs.RemoveAll(isoTmpDir) })
|
|
|
|
rootDir := filepath.Join(isoTmpDir, "rootfs")
|
|
err = utils.MkdirAll(b.cfg.Fs, rootDir, constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uefiDir := filepath.Join(isoTmpDir, "uefi")
|
|
err = utils.MkdirAll(b.cfg.Fs, uefiDir, constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isoDir := filepath.Join(isoTmpDir, "iso")
|
|
err = utils.MkdirAll(b.cfg.Fs, isoDir, constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.cfg.OutDir != "" {
|
|
err = utils.MkdirAll(b.cfg.Fs, b.cfg.OutDir, constants.DirPerm)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed creating output folder: %s", b.cfg.OutDir)
|
|
return err
|
|
}
|
|
}
|
|
|
|
b.cfg.Logger.Infof("Preparing squashfs root...")
|
|
err = b.applySources(rootDir, b.spec.RootFS...)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed installing OS packages: %v", err)
|
|
return err
|
|
}
|
|
err = utils.CreateDirStructure(b.cfg.Fs, rootDir)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed creating root directory structure: %v", err)
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Infof("Preparing EFI image...")
|
|
err = b.applySources(uefiDir, b.spec.UEFI...)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed installing EFI packages: %v", err)
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Infof("Preparing ISO image root tree...")
|
|
err = b.applySources(isoDir, b.spec.Image...)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed installing ISO image packages: %v", err)
|
|
return err
|
|
}
|
|
|
|
err = b.prepareISORoot(isoDir, rootDir, uefiDir)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Infof("Creating ISO image...")
|
|
err = b.burnISO(isoDir)
|
|
if err != nil {
|
|
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (b BuildISOAction) prepareISORoot(isoDir string, rootDir string, uefiDir string) error {
|
|
kernel, initrd, err := b.e.FindKernelInitrd(rootDir)
|
|
if err != nil {
|
|
b.cfg.Logger.Error("Could not find kernel and/or initrd")
|
|
return err
|
|
}
|
|
err = utils.MkdirAll(b.cfg.Fs, filepath.Join(isoDir, "boot"), constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
//TODO document boot/kernel and boot/initrd expectation in bootloader config
|
|
b.cfg.Logger.Debugf("Copying Kernel file %s to iso root tree", kernel)
|
|
err = utils.CopyFile(b.cfg.Fs, kernel, filepath.Join(isoDir, constants.IsoKernelPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Debugf("Copying initrd file %s to iso root tree", initrd)
|
|
err = utils.CopyFile(b.cfg.Fs, initrd, filepath.Join(isoDir, constants.IsoInitrdPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Info("Creating squashfs...")
|
|
err = utils.CreateSquashFS(b.cfg.Runner, b.cfg.Logger, rootDir, filepath.Join(isoDir, constants.IsoRootFile), constants.GetDefaultSquashfsOptions())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.cfg.Logger.Info("Creating EFI image...")
|
|
err = b.createEFI(uefiDir, filepath.Join(isoDir, constants.IsoEFIPath))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b BuildISOAction) createEFI(root string, img string) error {
|
|
efiSize, err := utils.DirSize(b.cfg.Fs, root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// align efiSize to the next 4MB slot
|
|
align := int64(4 * 1024 * 1024)
|
|
efiSizeMB := (efiSize/align*align + align) / (1024 * 1024)
|
|
|
|
err = b.e.CreateFileSystemImage(&v1.Image{
|
|
File: img,
|
|
Size: uint(efiSizeMB),
|
|
FS: constants.EfiFs,
|
|
Label: constants.EfiLabel,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
files, err := b.cfg.Fs.ReadDir(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range files {
|
|
_, err = b.cfg.Runner.Run("mcopy", "-s", "-i", img, filepath.Join(root, f.Name()), "::")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b BuildISOAction) burnISO(root string) error {
|
|
cmd := "xorriso"
|
|
var outputFile string
|
|
var isoFileName string
|
|
|
|
if b.cfg.Date {
|
|
currTime := time.Now()
|
|
isoFileName = fmt.Sprintf("%s.%s.iso", b.cfg.Name, currTime.Format("20060102"))
|
|
} else {
|
|
isoFileName = fmt.Sprintf("%s.iso", b.cfg.Name)
|
|
}
|
|
|
|
outputFile = isoFileName
|
|
if b.cfg.OutDir != "" {
|
|
outputFile = filepath.Join(b.cfg.OutDir, outputFile)
|
|
}
|
|
|
|
if exists, _ := utils.Exists(b.cfg.Fs, outputFile); exists {
|
|
b.cfg.Logger.Warnf("Overwriting already existing %s", outputFile)
|
|
err := b.cfg.Fs.Remove(outputFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-volid", b.spec.Label, "-joliet", "on", "-padding", "0",
|
|
"-outdev", outputFile, "-map", root, "/", "-chmod", "0755", "--",
|
|
}
|
|
args = append(args, constants.GetXorrisoBooloaderArgs(root)...)
|
|
|
|
out, err := b.cfg.Runner.Run(cmd, args...)
|
|
b.cfg.Logger.Debugf("Xorriso: %s", string(out))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checksum, err := utils.CalcFileChecksum(b.cfg.Fs, outputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("checksum computation failed: %w", err)
|
|
}
|
|
err = b.cfg.Fs.WriteFile(fmt.Sprintf("%s.sha256", outputFile), []byte(fmt.Sprintf("%s %s\n", checksum, isoFileName)), 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot write checksum file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b BuildISOAction) applySources(target string, sources ...*v1.ImageSource) error {
|
|
for _, src := range sources {
|
|
_, err := b.e.DumpSource(target, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *BuildISOAction) PrepareEFI(rootDir, uefiDir string) error {
|
|
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(uefiDir, constants.EfiBootPath), constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch g.cfg.Arch {
|
|
case constants.ArchAmd64, constants.Archx86:
|
|
err = utils.CopyFile(
|
|
g.cfg.Fs,
|
|
filepath.Join(rootDir, constants.GrubEfiImagex86),
|
|
filepath.Join(uefiDir, constants.GrubEfiImagex86Dest),
|
|
)
|
|
case constants.ArchArm64:
|
|
err = utils.CopyFile(
|
|
g.cfg.Fs,
|
|
filepath.Join(rootDir, constants.GrubEfiImageArm64),
|
|
filepath.Join(uefiDir, constants.GrubEfiImageArm64Dest),
|
|
)
|
|
default:
|
|
err = fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return g.cfg.Fs.WriteFile(filepath.Join(uefiDir, constants.EfiBootPath, constants.GrubCfg), []byte(constants.GrubEfiCfg), constants.FilePerm)
|
|
}
|
|
|
|
func (g *BuildISOAction) PrepareISO(rootDir, imageDir string) error {
|
|
|
|
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(imageDir, constants.GrubPrefixDir), constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch g.cfg.Arch {
|
|
case constants.ArchAmd64, constants.Archx86:
|
|
// Create eltorito image
|
|
eltorito, err := g.BuildEltoritoImg(rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Inlude loaders in expected paths
|
|
loaderDir := filepath.Join(imageDir, constants.IsoLoaderPath)
|
|
err = utils.MkdirAll(g.cfg.Fs, loaderDir, constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
loaderFiles := []string{eltorito, constants.GrubBootHybridImg}
|
|
loaderFiles = append(loaderFiles, strings.Split(constants.SyslinuxFiles, " ")...)
|
|
for _, f := range loaderFiles {
|
|
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, f), loaderDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
fontsDir := filepath.Join(loaderDir, "/grub2/fonts")
|
|
err = utils.MkdirAll(g.cfg.Fs, fontsDir, constants.DirPerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, constants.GrubFont), fontsDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case constants.ArchArm64:
|
|
// TBC
|
|
default:
|
|
return fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
|
|
}
|
|
|
|
// Write grub.cfg file
|
|
err = g.cfg.Fs.WriteFile(
|
|
filepath.Join(imageDir, constants.GrubPrefixDir, constants.GrubCfg),
|
|
[]byte(fmt.Sprintf(constants.GrubCfgTemplate, g.spec.GrubEntry, g.spec.Label)),
|
|
constants.FilePerm,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Include EFI contents in iso root too
|
|
return g.PrepareEFI(rootDir, imageDir)
|
|
}
|
|
|
|
func (g *BuildISOAction) BuildEltoritoImg(rootDir string) (string, error) {
|
|
var args []string
|
|
args = append(args, "-O", constants.GrubBiosTarget)
|
|
args = append(args, "-o", constants.GrubBiosImg)
|
|
args = append(args, "-p", constants.GrubPrefixDir)
|
|
args = append(args, "-d", constants.GrubI386BinDir)
|
|
args = append(args, strings.Split(constants.GrubModules, " ")...)
|
|
|
|
chRoot := utils.NewChroot(rootDir, &g.cfg.Config)
|
|
out, err := chRoot.Run("grub2-mkimage", args...)
|
|
if err != nil {
|
|
g.cfg.Logger.Errorf("grub2-mkimage failed: %s", string(out))
|
|
g.cfg.Logger.Errorf("Error: %v", err)
|
|
return "", err
|
|
}
|
|
|
|
concatFiles := func() error {
|
|
return utils.ConcatFiles(
|
|
g.cfg.Fs, []string{constants.GrubBiosCDBoot, constants.GrubBiosImg},
|
|
constants.GrubEltoritoImg,
|
|
)
|
|
}
|
|
err = chRoot.RunCallback(concatFiles)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return constants.GrubEltoritoImg, nil
|
|
}
|