588 lines
17 KiB
Go
Raw Normal View History

// 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 install
import (
"bytes"
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"slices"
"github.com/google/uuid"
"github.com/siderolabs/go-blockdevice/v2/blkid"
"github.com/siderolabs/go-blockdevice/v2/block"
"github.com/siderolabs/go-blockdevice/v2/partitioning"
"github.com/siderolabs/go-blockdevice/v2/partitioning/gpt"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs"
"gopkg.in/yaml.v3"
"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/bootloader"
bootloaderoptions "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options"
"github.com/siderolabs/talos/internal/pkg/meta"
"github.com/siderolabs/talos/internal/pkg/partition"
"github.com/siderolabs/talos/pkg/imager/overlay/executor"
"github.com/siderolabs/talos/pkg/machinery/compatibility"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
"github.com/siderolabs/talos/pkg/machinery/kernel"
metaconsts "github.com/siderolabs/talos/pkg/machinery/meta"
"github.com/siderolabs/talos/pkg/machinery/overlay"
"github.com/siderolabs/talos/pkg/machinery/version"
)
// Options represents the set of options available for an install.
type Options struct {
ConfigSource string
Disk string
Platform string
Arch string
Board string
ExtraKernelArgs []string
Upgrade bool
Force bool
Zero bool
LegacyBIOSSupport bool
MetaValues MetaValues
OverlayInstaller overlay.Installer[overlay.ExtraOptions]
OverlayName string
OverlayExtractedDir string
ExtraOptions overlay.ExtraOptions
// Options specific for the image creation mode.
ImageSecureboot bool
Version string
BootAssets bootloaderoptions.BootAssets
Printf func(string, ...any)
MountPrefix string
}
// Mode is the install mode.
type Mode int
const (
// ModeInstall is the install mode.
ModeInstall Mode = iota
// ModeUpgrade is the upgrade mode.
ModeUpgrade
// ModeImage is the image creation mode.
ModeImage
)
// IsImage returns true if the mode is image creation.
func (m Mode) IsImage() bool {
return m == ModeImage
}
const typeGPT = "gpt"
// Install installs Talos.
//
//nolint:gocyclo
func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) error {
overlayPresent := overlayPresent()
if b := getBoard(); b != constants.BoardNone && !overlayPresent {
return fmt.Errorf("using standard installer image is not supported for board: %s, use an installer with overlay", b)
}
if overlayPresent {
extraOptionsBytes, err := os.ReadFile(constants.ImagerOverlayExtraOptionsPath)
if err != nil {
return err
}
var extraOptions overlay.ExtraOptions
decoder := yaml.NewDecoder(bytes.NewReader(extraOptionsBytes))
decoder.KnownFields(true)
if err := decoder.Decode(&extraOptions); err != nil {
return fmt.Errorf("failed to decode extra options: %w", err)
}
opts.OverlayInstaller = executor.New(constants.ImagerOverlayInstallerDefaultPath)
opts.ExtraOptions = extraOptions
}
cmdline := procfs.NewCmdline("")
cmdline.Append(constants.KernelParamPlatform, p.Name())
if opts.ConfigSource != "" {
cmdline.Append(constants.KernelParamConfig, opts.ConfigSource)
}
cmdline.SetAll(p.KernelArgs(opts.Arch).Strings())
// 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 opts.Board != constants.BoardNone {
// board 'rpi_4' was removed in Talos 1.5 in favor of `rpi_generic`
if opts.Board == "rpi_4" {
opts.Board = constants.BoardRPiGeneric
}
var b runtime.Board
b, err := board.NewBoard(opts.Board)
if err != nil {
return err
}
cmdline.Append(constants.KernelParamBoard, b.Name())
cmdline.SetAll(b.KernelArgs().Strings())
}
if opts.OverlayInstaller != nil {
overlayOpts, getOptsErr := opts.OverlayInstaller.GetOptions(opts.ExtraOptions)
if getOptsErr != nil {
return fmt.Errorf("failed to get overlay installer options: %w", getOptsErr)
}
opts.OverlayName = overlayOpts.Name
cmdline.SetAll(overlayOpts.KernelArgs)
}
// preserve console=ttyS0 if it was already present in cmdline for metal platform
existingCmdline := procfs.ProcCmdline()
if *existingCmdline.Get(constants.KernelParamPlatform).First() == constants.PlatformMetal && existingCmdline.Get("console").Contains("ttyS0") {
if !slices.Contains(opts.ExtraKernelArgs, "console=ttyS0") {
cmdline.Append("console", "ttyS0")
}
}
if err := cmdline.AppendAll(
opts.ExtraKernelArgs,
procfs.WithOverwriteArgs("console"),
procfs.WithOverwriteArgs(constants.KernelParamPlatform),
procfs.WithDeleteNegatedArgs(),
); err != nil {
return err
}
i, err := NewInstaller(ctx, cmdline, mode, opts)
if err != nil {
return err
}
if err = i.Install(ctx, mode); err != nil {
return err
}
i.options.Printf("installation of %s complete", version.Tag)
return nil
}
// Installer represents the installer logic. It serves as the entrypoint to all
// installation methods.
type Installer struct {
cmdline *procfs.Cmdline
options *Options
}
// NewInstaller initializes and returns an Installer.
func NewInstaller(ctx context.Context, cmdline *procfs.Cmdline, mode Mode, opts *Options) (i *Installer, err error) {
i = &Installer{
cmdline: cmdline,
options: opts,
}
if i.options.Version == "" {
i.options.Version = version.Tag
}
if i.options.Printf == nil {
i.options.Printf = log.Printf
}
if mode == ModeUpgrade && i.options.Force {
i.options.Printf("system disk wipe on upgrade is not supported anymore, option ignored")
}
if i.options.Zero && mode != ModeInstall {
i.options.Printf("zeroing of the disk is only supported for the initial installation, option ignored")
}
i.options.BootAssets.FillDefaults(opts.Arch)
return i, nil
}
// Install fetches the necessary data locations and copies or extracts
// to the target locations.
//
//nolint:gocyclo,cyclop
func (i *Installer) Install(ctx context.Context, mode Mode) (err error) {
// pre-flight checks, erratas
hostTalosVersion, err := readHostTalosVersion()
if err != nil {
return err
}
if mode == ModeUpgrade {
errataArm64ZBoot()
i.errataNetIfnames(hostTalosVersion)
}
if err = i.runPreflightChecks(mode); err != nil {
return err
}
// prepare extensions if legacy machine.install.extensions is present
if err = i.installExtensions(); err != nil {
return err
}
// open and lock the blockdevice for the installation disk for the whole duration of the installer
bd, err := block.NewFromPath(i.options.Disk, block.OpenForWrite())
if err != nil {
return fmt.Errorf("failed to open blockdevice %s: %w", i.options.Disk, err)
}
defer bd.Close() //nolint:errcheck
if err = bd.Lock(true); err != nil {
return fmt.Errorf("failed to lock blockdevice %s: %w", i.options.Disk, err)
}
defer bd.Unlock() //nolint:errcheck
var bootlder bootloader.Bootloader
// if running in install/image mode, just create new GPT partition table
// if running in upgrade mode, verify that install disk has correct GPT partition table and expected partitions
// probe the disk anyways
info, err := blkid.Probe(bd.File(), blkid.WithSkipLocking(true))
if err != nil {
return fmt.Errorf("failed to probe blockdevice %s: %w", i.options.Disk, err)
}
gptdev, err := gpt.DeviceFromBlockDevice(bd)
if err != nil {
return fmt.Errorf("error getting GPT device: %w", err)
}
switch mode {
case ModeImage:
// on image creation, we don't care about disk contents
bootlder = bootloader.New(i.options.ImageSecureboot, i.options.Version)
case ModeInstall:
if !i.options.Zero && !i.options.Force {
// verify that the disk is either empty or has an empty GPT partition table, otherwise fail the install
switch {
case info.Name == "":
// empty, ok
case info.Name == typeGPT && len(info.Parts) == 0:
// GPT, no partitions, ok
default:
return fmt.Errorf("disk %s is not empty, skipping install, detected %q", i.options.Disk, info.Name)
}
} else {
// zero the disk
if err = bd.FastWipe(); err != nil {
return fmt.Errorf("failed to zero blockdevice %s: %w", i.options.Disk, err)
}
}
// on install, automatically detect the bootloader
bootlder = bootloader.NewAuto()
case ModeUpgrade:
// on upgrade, we don't touch the disk partitions, but we need to verify that the disk has the expected GPT partition table
if info.Name != typeGPT {
return fmt.Errorf("disk %s has an unexpected format %q", i.options.Disk, info.Name)
}
}
if mode == ModeImage || mode == ModeInstall {
// create partitions and re-probe the device
info, err = i.createPartitions(gptdev, mode, hostTalosVersion, bootlder)
if err != nil {
return fmt.Errorf("failed to create partitions: %w", err)
}
}
if mode == ModeUpgrade {
// on upgrade, probe the bootloader
bootlder, err = bootloader.Probe(i.options.Disk, bootloaderoptions.ProbeOptions{
// the disk is already locked
BlockProbeOptions: []blkid.ProbeOption{
blkid.WithSkipLocking(true),
},
})
if err != nil {
return fmt.Errorf("failed to probe bootloader on upgrade: %w", err)
}
}
// Install the bootloader.
bootInstallResult, err := bootlder.Install(bootloaderoptions.InstallOptions{
BootDisk: i.options.Disk,
Arch: i.options.Arch,
Cmdline: i.cmdline.String(),
Version: i.options.Version,
ImageMode: mode.IsImage(),
MountPrefix: i.options.MountPrefix,
BootAssets: i.options.BootAssets,
Printf: i.options.Printf,
BlkidInfo: info,
ExtraInstallStep: func() error {
if i.options.Board != constants.BoardNone {
var b runtime.Board
b, err = board.NewBoard(i.options.Board)
if err != nil {
return err
}
i.options.Printf("installing U-Boot for %q", b.Name())
if err = b.Install(runtime.BoardInstallOptions{
InstallDisk: i.options.Disk,
MountPrefix: i.options.MountPrefix,
UBootPath: i.options.BootAssets.UBootPath,
DTBPath: i.options.BootAssets.DTBPath,
RPiFirmwarePath: i.options.BootAssets.RPiFirmwarePath,
Printf: i.options.Printf,
}); err != nil {
return fmt.Errorf("failed to install for board %s: %w", b.Name(), err)
}
}
if i.options.OverlayInstaller != nil {
i.options.Printf("running overlay installer %q", i.options.OverlayName)
if err = i.options.OverlayInstaller.Install(overlay.InstallOptions[overlay.ExtraOptions]{
InstallDisk: i.options.Disk,
MountPrefix: i.options.MountPrefix,
ArtifactsPath: filepath.Join(i.options.OverlayExtractedDir, constants.ImagerOverlayArtifactsPath),
ExtraOptions: i.options.ExtraOptions,
}); err != nil {
return fmt.Errorf("failed to run overlay installer: %w", err)
}
}
return nil
},
})
if err != nil {
return fmt.Errorf("failed to install bootloader: %w", err)
}
if mode == ModeUpgrade || len(i.options.MetaValues.values) > 0 {
var (
metaState *meta.Meta
metaPartitionName string
)
for _, partition := range info.Parts {
if pointer.SafeDeref(partition.PartitionLabel) == constants.MetaPartitionLabel {
metaPartitionName = partitioning.DevName(i.options.Disk, partition.PartitionIndex)
break
}
}
if metaPartitionName == "" {
return errors.New("failed to detect META partition")
}
if metaState, err = meta.New(ctx, nil, meta.WithPrinter(i.options.Printf), meta.WithFixedPath(metaPartitionName)); err != nil {
return fmt.Errorf("failed to open META: %w", err)
}
var ok bool
if mode == ModeUpgrade {
if ok, err = metaState.SetTag(ctx, metaconsts.Upgrade, bootInstallResult.PreviousLabel); !ok || err != nil {
return fmt.Errorf("failed to set upgrade tag: %q", bootInstallResult.PreviousLabel)
}
}
for _, v := range i.options.MetaValues.values {
if ok, err = metaState.SetTag(ctx, v.Key, v.Value); !ok || err != nil {
return fmt.Errorf("failed to set meta tag: %q -> %q", v.Key, v.Value)
}
}
if err = metaState.Flush(); err != nil {
return fmt.Errorf("failed to flush META: %w", err)
}
}
return nil
}
//nolint:gocyclo,cyclop
func (i *Installer) createPartitions(gptdev gpt.Device, mode Mode, hostTalosVersion *compatibility.TalosVersion, bootlder bootloader.Bootloader) (*blkid.Info, error) {
var partitionOptions *runtime.PartitionOptions
if i.options.Board != constants.BoardNone && !quirks.New(i.options.Version).SupportsOverlay() {
var b runtime.Board
b, err := board.NewBoard(i.options.Board)
if err != nil {
return nil, err
}
partitionOptions = b.PartitionOptions()
}
if i.options.OverlayInstaller != nil {
overlayOpts, getOptsErr := i.options.OverlayInstaller.GetOptions(i.options.ExtraOptions)
if getOptsErr != nil {
return nil, fmt.Errorf("failed to get overlay installer options: %w", getOptsErr)
}
if overlayOpts.PartitionOptions.Offset != 0 {
partitionOptions = &runtime.PartitionOptions{
PartitionsOffset: overlayOpts.PartitionOptions.Offset,
}
}
}
var gptOptions []gpt.Option
if partitionOptions != nil && partitionOptions.PartitionsOffset != 0 {
gptOptions = append(gptOptions, gpt.WithSkipLBAs(uint(partitionOptions.PartitionsOffset)))
}
if i.options.LegacyBIOSSupport {
gptOptions = append(gptOptions, gpt.WithMarkPMBRBootable())
}
pt, err := gpt.New(gptdev, gptOptions...)
if err != nil {
return nil, fmt.Errorf("failed to initialize GPT: %w", err)
}
// boot partitions
partitions := bootlder.RequiredPartitions()
// META partition
partitions = append(partitions,
partition.NewPartitionOptions(constants.MetaPartitionLabel, false),
)
legacyImage := mode == ModeImage && !quirks.New(i.options.Version).SkipDataPartitions()
// compatibility when installing on Talos < 1.8
if legacyImage || (hostTalosVersion != nil && hostTalosVersion.PrecreateStatePartition()) {
partitions = append(partitions,
partition.NewPartitionOptions(constants.StatePartitionLabel, false),
)
}
if legacyImage {
partitions = append(partitions,
partition.NewPartitionOptions(constants.EphemeralPartitionLabel, false),
)
}
for _, p := range partitions {
size := p.Size
if size == 0 {
size = pt.LargestContiguousAllocatable()
}
partitionTyp := uuid.MustParse(p.PartitionType)
_, _, err = pt.AllocatePartition(size, p.PartitionLabel, partitionTyp, p.PartitionOpts...)
if err != nil {
return nil, fmt.Errorf("failed to allocate partition %s: %w", p.PartitionLabel, err)
}
i.options.Printf("created %s (%s) size %d bytes", p.PartitionLabel, p.PartitionType, size)
}
if err = pt.Write(); err != nil {
return nil, fmt.Errorf("failed to write GPT: %w", err)
}
// now format all partitions
for idx, p := range partitions {
devName := partitioning.DevName(i.options.Disk, uint(idx+1))
if err = partition.Format(devName, &p.FormatOptions, i.options.Printf); err != nil {
return nil, fmt.Errorf("failed to format partition %s: %w", devName, err)
}
}
info, err := blkid.ProbePath(i.options.Disk, blkid.WithSkipLocking(true))
if err != nil {
return nil, fmt.Errorf("failed to probe blockdevice %s: %w", i.options.Disk, err)
}
if len(info.Parts) != len(partitions) {
return nil, fmt.Errorf("expected %d partitions, got %d", len(partitions), len(info.Parts))
}
// this is weird, but sometimes blkid doesn't return the filesystem type for freshly formatted partitions
for idx, p := range partitions {
if p.FormatOptions.FileSystemType == partition.FilesystemTypeNone || p.FormatOptions.FileSystemType == partition.FilesystemTypeZeroes {
continue
}
info.Parts[idx].Name = p.FormatOptions.FileSystemType
}
return info, nil
}
func (i *Installer) runPreflightChecks(mode Mode) error {
if mode != ModeUpgrade {
// pre-flight checks only apply to upgrades
return nil
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
checks, err := NewPreflightChecks(ctx)
if err != nil {
return fmt.Errorf("error initializing pre-flight checks: %w", err)
}
defer checks.Close() //nolint:errcheck
return checks.Run(ctx)
}
func overlayPresent() bool {
_, err := os.Stat(constants.ImagerOverlayInstallerDefaultPath)
return err == nil
}
func getBoard() string {
cmdline := procfs.ProcCmdline()
if cmdline == nil {
return constants.BoardNone
}
board := cmdline.Get(constants.KernelParamBoard)
if board == nil {
return constants.BoardNone
}
return *board.First()
}