Fix Equnix Metal (where proper arm64 args are known) and metal platform (using generic arm64 console arg). Other platforms might need to be updated, but correct settings are not known at the moment. Fixes #8529 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
429 lines
12 KiB
Go
429 lines
12 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Package imager contains code related to generation of different boot assets for Talos Linux.
|
|
package imager
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/siderolabs/go-procfs/procfs"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
talosruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/board"
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
|
|
"github.com/siderolabs/talos/internal/pkg/secureboot/uki"
|
|
"github.com/siderolabs/talos/pkg/imager/extensions"
|
|
"github.com/siderolabs/talos/pkg/imager/overlay/executor"
|
|
"github.com/siderolabs/talos/pkg/imager/profile"
|
|
"github.com/siderolabs/talos/pkg/imager/quirks"
|
|
"github.com/siderolabs/talos/pkg/imager/utils"
|
|
"github.com/siderolabs/talos/pkg/machinery/config/merge"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
"github.com/siderolabs/talos/pkg/machinery/kernel"
|
|
"github.com/siderolabs/talos/pkg/machinery/overlay"
|
|
"github.com/siderolabs/talos/pkg/machinery/version"
|
|
"github.com/siderolabs/talos/pkg/reporter"
|
|
)
|
|
|
|
// Imager is an interface for image generation.
|
|
type Imager struct {
|
|
prof profile.Profile
|
|
|
|
overlayInstaller overlay.Installer[overlay.ExtraOptions]
|
|
|
|
tempDir string
|
|
|
|
// boot assets
|
|
initramfsPath string
|
|
cmdline string
|
|
|
|
sdBootPath string
|
|
ukiPath string
|
|
}
|
|
|
|
// New creates a new Imager.
|
|
func New(prof profile.Profile) (*Imager, error) {
|
|
return &Imager{
|
|
prof: prof,
|
|
}, nil
|
|
}
|
|
|
|
// Execute image generation.
|
|
//
|
|
//nolint:gocyclo,cyclop
|
|
func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporter.Reporter) (outputAssetPath string, err error) {
|
|
i.tempDir, err = os.MkdirTemp("", "imager")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temporary directory: %w", err)
|
|
}
|
|
|
|
defer os.RemoveAll(i.tempDir) //nolint:errcheck
|
|
|
|
// 0. Handle overlays first
|
|
if err = i.handleOverlay(ctx, report); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err = i.handleProf(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
report.Report(reporter.Update{
|
|
Message: "profile ready:",
|
|
Status: reporter.StatusSucceeded,
|
|
})
|
|
|
|
// 1. Dump the profile.
|
|
if err = i.prof.Dump(os.Stderr); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 2. Transform `initramfs.xz` with system extensions
|
|
if err = i.buildInitramfs(ctx, report); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 3. Prepare kernel arguments.
|
|
if err = i.buildCmdline(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
report.Report(reporter.Update{
|
|
Message: fmt.Sprintf("kernel command line: %s", i.cmdline),
|
|
Status: reporter.StatusSucceeded,
|
|
})
|
|
|
|
// 4. Build UKI if Secure Boot is enabled.
|
|
if i.prof.SecureBootEnabled() {
|
|
if err = i.buildUKI(ctx, report); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// 5. Build the output.
|
|
outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath())
|
|
|
|
switch i.prof.Output.Kind {
|
|
case profile.OutKindISO:
|
|
err = i.outISO(ctx, outputAssetPath, report)
|
|
case profile.OutKindKernel:
|
|
err = i.outKernel(outputAssetPath, report)
|
|
case profile.OutKindUKI:
|
|
err = i.outUKI(outputAssetPath, report)
|
|
case profile.OutKindInitramfs:
|
|
err = i.outInitramfs(outputAssetPath, report)
|
|
case profile.OutKindCmdline:
|
|
err = i.outCmdline(outputAssetPath)
|
|
case profile.OutKindImage:
|
|
err = i.outImage(ctx, outputAssetPath, report)
|
|
case profile.OutKindInstaller:
|
|
err = i.outInstaller(ctx, outputAssetPath, report)
|
|
case profile.OutKindUnknown:
|
|
fallthrough
|
|
default:
|
|
return "", fmt.Errorf("unknown output kind: %s", i.prof.Output.Kind)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
report.Report(reporter.Update{
|
|
Message: fmt.Sprintf("output asset path: %s", outputAssetPath),
|
|
Status: reporter.StatusSucceeded,
|
|
})
|
|
|
|
// 6. Post-process the output.
|
|
switch i.prof.Output.OutFormat {
|
|
case profile.OutFormatRaw:
|
|
// do nothing
|
|
return outputAssetPath, nil
|
|
case profile.OutFormatXZ:
|
|
return i.postProcessXz(outputAssetPath, report)
|
|
case profile.OutFormatGZ:
|
|
return i.postProcessGz(outputAssetPath, report)
|
|
case profile.OutFormatTar:
|
|
return i.postProcessTar(outputAssetPath, report)
|
|
case profile.OutFormatUnknown:
|
|
fallthrough
|
|
default:
|
|
return "", fmt.Errorf("unknown output format: %s", i.prof.Output.OutFormat)
|
|
}
|
|
}
|
|
|
|
func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) error {
|
|
if i.prof.Overlay == nil {
|
|
report.Report(reporter.Update{
|
|
Message: "skipped pulling overlay (no overlay)",
|
|
Status: reporter.StatusSkip,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
tempOverlayPath := filepath.Join(i.tempDir, constants.ImagerOverlayBasePath)
|
|
|
|
if err := os.MkdirAll(tempOverlayPath, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create overlay directory: %w", err)
|
|
}
|
|
|
|
if err := i.prof.Overlay.Image.Extract(ctx, tempOverlayPath, runtime.GOARCH, progressPrintf(report, reporter.Update{Message: "pulling overlay...", Status: reporter.StatusRunning})); err != nil {
|
|
return err
|
|
}
|
|
|
|
// find all *.yaml files in the overlay/profiles/ directory
|
|
profileYAMLs, err := filepath.Glob(filepath.Join(i.tempDir, constants.ImagerOverlayProfilesPath, "*.yaml"))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find profiles: %w", err)
|
|
}
|
|
|
|
if i.prof.Overlay.Name == "" {
|
|
i.prof.Overlay.Name = constants.ImagerOverlayInstallerDefault
|
|
}
|
|
|
|
i.overlayInstaller = executor.New(filepath.Join(i.tempDir, constants.ImagerOverlayInstallersPath, i.prof.Overlay.Name))
|
|
|
|
for _, profilePath := range profileYAMLs {
|
|
profileName := strings.TrimSuffix(filepath.Base(profilePath), ".yaml")
|
|
|
|
var overlayProfile profile.Profile
|
|
|
|
profileDataBytes, err := os.ReadFile(profilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read profile: %w", err)
|
|
}
|
|
|
|
if err := yaml.Unmarshal(profileDataBytes, &overlayProfile); err != nil {
|
|
return fmt.Errorf("failed to unmarshal profile: %w", err)
|
|
}
|
|
|
|
profile.Default[profileName] = overlayProfile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Imager) handleProf() error {
|
|
// resolve the profile if it contains a base name
|
|
if i.prof.BaseProfileName != "" {
|
|
baseProfile, ok := profile.Default[i.prof.BaseProfileName]
|
|
if !ok {
|
|
return fmt.Errorf("unknown base profile: %s", i.prof.BaseProfileName)
|
|
}
|
|
|
|
baseProfile = baseProfile.DeepCopy()
|
|
|
|
// merge the profiles
|
|
if err := merge.Merge(&baseProfile, &i.prof); err != nil {
|
|
return err
|
|
}
|
|
|
|
i.prof = baseProfile
|
|
i.prof.BaseProfileName = ""
|
|
}
|
|
|
|
if i.prof.Version == "" {
|
|
i.prof.Version = version.Tag
|
|
}
|
|
|
|
if err := i.prof.Validate(); err != nil {
|
|
return fmt.Errorf("profile is invalid: %w", err)
|
|
}
|
|
|
|
i.prof.Input.FillDefaults(i.prof.Arch, i.prof.Version, i.prof.SecureBootEnabled())
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildInitramfs transforms `initramfs.xz` with system extensions.
|
|
func (i *Imager) buildInitramfs(ctx context.Context, report *reporter.Reporter) error {
|
|
if len(i.prof.Input.SystemExtensions) == 0 {
|
|
report.Report(reporter.Update{
|
|
Message: "skipped initramfs rebuild (no system extensions)",
|
|
Status: reporter.StatusSkip,
|
|
})
|
|
|
|
// no system extensions, happy path
|
|
i.initramfsPath = i.prof.Input.Initramfs.Path
|
|
|
|
return nil
|
|
}
|
|
|
|
if i.prof.Output.Kind == profile.OutKindCmdline || i.prof.Output.Kind == profile.OutKindKernel {
|
|
// these outputs don't use initramfs image
|
|
return nil
|
|
}
|
|
|
|
printf := progressPrintf(report, reporter.Update{Message: "rebuilding initramfs with system extensions...", Status: reporter.StatusRunning})
|
|
|
|
// copy the initramfs to a temporary location, as it's going to be modified during the extension build process
|
|
tempInitramfsPath := filepath.Join(i.tempDir, "initramfs.xz")
|
|
|
|
if err := utils.CopyFiles(printf, utils.SourceDestination(i.prof.Input.Initramfs.Path, tempInitramfsPath)); err != nil {
|
|
return fmt.Errorf("failed to copy initramfs: %w", err)
|
|
}
|
|
|
|
i.initramfsPath = tempInitramfsPath
|
|
|
|
extensionsCheckoutDir := filepath.Join(i.tempDir, "extensions")
|
|
|
|
// pull every extension to a temporary location
|
|
for j, ext := range i.prof.Input.SystemExtensions {
|
|
extensionDir := filepath.Join(extensionsCheckoutDir, strconv.Itoa(j))
|
|
|
|
if err := os.MkdirAll(extensionDir, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create extension directory: %w", err)
|
|
}
|
|
|
|
if err := ext.Extract(ctx, extensionDir, i.prof.Arch, printf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// rebuild initramfs
|
|
builder := extensions.Builder{
|
|
InitramfsPath: i.initramfsPath,
|
|
Arch: i.prof.Arch,
|
|
ExtensionTreePath: extensionsCheckoutDir,
|
|
Printf: printf,
|
|
}
|
|
|
|
if err := builder.Build(); err != nil {
|
|
return err
|
|
}
|
|
|
|
report.Report(reporter.Update{
|
|
Message: "initramfs ready",
|
|
Status: reporter.StatusSucceeded,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildCmdline builds the kernel command line.
|
|
//
|
|
//nolint:gocyclo
|
|
func (i *Imager) buildCmdline() error {
|
|
p, err := platform.NewPlatform(i.prof.Platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmdline := procfs.NewCmdline("")
|
|
|
|
// platform kernel args
|
|
cmdline.Append(constants.KernelParamPlatform, p.Name())
|
|
cmdline.SetAll(p.KernelArgs(i.prof.Arch).Strings())
|
|
|
|
// board kernel args
|
|
if i.prof.Board != "" && !quirks.New(i.prof.Version).SupportsOverlay() {
|
|
var b talosruntime.Board
|
|
|
|
b, err = board.NewBoard(i.prof.Board)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmdline.Append(constants.KernelParamBoard, b.Name())
|
|
cmdline.SetAll(b.KernelArgs().Strings())
|
|
}
|
|
|
|
// overlay kernel args
|
|
if i.overlayInstaller != nil {
|
|
options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.ExtraOptions)
|
|
if optsErr != nil {
|
|
return optsErr
|
|
}
|
|
|
|
cmdline.SetAll(options.KernelArgs)
|
|
}
|
|
|
|
// first defaults, then extra kernel args to allow extra kernel args to override defaults
|
|
if err = cmdline.AppendAll(kernel.DefaultArgs); err != nil {
|
|
return err
|
|
}
|
|
|
|
if i.prof.SecureBootEnabled() {
|
|
if err = cmdline.AppendAll(kernel.SecureBootArgs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// meta values can be written only to the "image" output
|
|
if len(i.prof.Customization.MetaContents) > 0 && i.prof.Output.Kind != profile.OutKindImage {
|
|
// pass META values as kernel talos.environment args which will be passed via the environment to the installer
|
|
cmdline.Append(
|
|
constants.KernelParamEnvironment,
|
|
constants.MetaValuesEnvVar+"="+i.prof.Customization.MetaContents.Encode(quirks.New(i.prof.Version).SupportsCompressedEncodedMETA()),
|
|
)
|
|
}
|
|
|
|
// apply customization
|
|
if err = cmdline.AppendAll(
|
|
i.prof.Customization.ExtraKernelArgs,
|
|
procfs.WithOverwriteArgs("console"),
|
|
procfs.WithOverwriteArgs(constants.KernelParamPlatform),
|
|
procfs.WithDeleteNegatedArgs(),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
i.cmdline = cmdline.String()
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildUKI assembles the UKI and signs it.
|
|
func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error {
|
|
printf := progressPrintf(report, reporter.Update{Message: "building UKI...", Status: reporter.StatusRunning})
|
|
|
|
i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed")
|
|
i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed")
|
|
|
|
pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get PCR signer: %w", err)
|
|
}
|
|
|
|
securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get SecureBoot signer: %w", err)
|
|
}
|
|
|
|
builder := uki.Builder{
|
|
Arch: i.prof.Arch,
|
|
Version: i.prof.Version,
|
|
SdStubPath: i.prof.Input.SDStub.Path,
|
|
SdBootPath: i.prof.Input.SDBoot.Path,
|
|
KernelPath: i.prof.Input.Kernel.Path,
|
|
InitrdPath: i.initramfsPath,
|
|
Cmdline: i.cmdline,
|
|
|
|
SecureBootSigner: securebootSigner,
|
|
PCRSigner: pcrSigner,
|
|
|
|
OutSdBootPath: i.sdBootPath,
|
|
OutUKIPath: i.ukiPath,
|
|
}
|
|
|
|
if err := builder.Build(printf); err != nil {
|
|
return err
|
|
}
|
|
|
|
report.Report(reporter.Update{
|
|
Message: "UKI ready",
|
|
Status: reporter.StatusSucceeded,
|
|
})
|
|
|
|
return nil
|
|
}
|