mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-07 07:07:10 +02:00
There's a cyclic dependency on siderolink library which imports talos machinery back. We will fix that after we get talos pushed under a new name. Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
562 lines
13 KiB
Go
562 lines
13 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 install
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/siderolabs/go-blockdevice/blockdevice"
|
|
"github.com/siderolabs/go-blockdevice/blockdevice/partition/gpt"
|
|
"github.com/siderolabs/go-retry/retry"
|
|
|
|
"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/pkg/mount"
|
|
"github.com/siderolabs/talos/internal/pkg/partition"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
)
|
|
|
|
// Manifest represents the instructions for preparing all block devices
|
|
// for an installation.
|
|
type Manifest struct {
|
|
PartitionOptions *runtime.PartitionOptions
|
|
Devices map[string]Device
|
|
Targets map[string][]*Target
|
|
LegacyBIOSSupport bool
|
|
}
|
|
|
|
// Device represents device options.
|
|
type Device struct {
|
|
Device string
|
|
|
|
ResetPartitionTable bool
|
|
Zero bool
|
|
|
|
SkipOverlayMountsCheck bool
|
|
}
|
|
|
|
// NewManifest initializes and returns a Manifest.
|
|
//
|
|
//nolint:gocyclo
|
|
func NewManifest(label string, sequence runtime.Sequence, bootPartitionFound bool, opts *Options) (manifest *Manifest, err error) {
|
|
if label == "" {
|
|
return nil, fmt.Errorf("a label is required, got \"\"")
|
|
}
|
|
|
|
manifest = &Manifest{
|
|
Devices: map[string]Device{},
|
|
Targets: map[string][]*Target{},
|
|
LegacyBIOSSupport: opts.LegacyBIOSSupport,
|
|
}
|
|
|
|
if opts.Board != constants.BoardNone {
|
|
var b runtime.Board
|
|
|
|
b, err = board.NewBoard(opts.Board)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manifest.PartitionOptions = b.PartitionOptions()
|
|
}
|
|
|
|
// TODO: legacy, to support old Talos initramfs, assume force if boot partition not found
|
|
if !bootPartitionFound {
|
|
opts.Force = true
|
|
}
|
|
|
|
if !opts.Force && opts.Zero {
|
|
return nil, fmt.Errorf("zero option can't be used without force")
|
|
}
|
|
|
|
if !opts.Force && !bootPartitionFound {
|
|
return nil, fmt.Errorf("install with preserve is not supported if existing boot partition was not found")
|
|
}
|
|
|
|
// Verify that the target device(s) can satisfy the requested options.
|
|
|
|
if sequence != runtime.SequenceUpgrade {
|
|
if err = VerifyEphemeralPartition(opts); err != nil {
|
|
return nil, fmt.Errorf("failed to prepare ephemeral partition: %w", err)
|
|
}
|
|
|
|
if err = VerifyBootPartition(opts); err != nil {
|
|
return nil, fmt.Errorf("failed to prepare boot partition: %w", err)
|
|
}
|
|
}
|
|
|
|
skipOverlayMountsCheck, err := shouldSkipOverlayMountsCheck(sequence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manifest.Devices[opts.Disk] = Device{
|
|
Device: opts.Disk,
|
|
|
|
ResetPartitionTable: opts.Force,
|
|
Zero: opts.Zero,
|
|
|
|
SkipOverlayMountsCheck: skipOverlayMountsCheck,
|
|
}
|
|
|
|
// Initialize any slices we need. Note that a boot partition is not
|
|
// required.
|
|
|
|
if manifest.Targets[opts.Disk] == nil {
|
|
manifest.Targets[opts.Disk] = []*Target{}
|
|
}
|
|
|
|
efiTarget := EFITarget(opts.Disk, nil)
|
|
biosTarget := BIOSTarget(opts.Disk, nil)
|
|
|
|
var bootTarget *Target
|
|
|
|
if opts.Bootloader {
|
|
bootTarget = BootTarget(opts.Disk, &Target{
|
|
PreserveContents: bootPartitionFound,
|
|
Assets: []*Asset{
|
|
{
|
|
Source: fmt.Sprintf(constants.KernelAssetPath, opts.Arch),
|
|
Destination: filepath.Join(constants.BootMountPoint, label, constants.KernelAsset),
|
|
},
|
|
{
|
|
Source: fmt.Sprintf(constants.InitramfsAssetPath, opts.Arch),
|
|
Destination: filepath.Join(constants.BootMountPoint, label, constants.InitramfsAsset),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
metaTarget := MetaTarget(opts.Disk, &Target{
|
|
PreserveContents: bootPartitionFound,
|
|
})
|
|
|
|
stateTarget := StateTarget(opts.Disk, &Target{
|
|
PreserveContents: bootPartitionFound,
|
|
FormatOptions: &partition.FormatOptions{
|
|
FileSystemType: partition.FilesystemTypeNone,
|
|
},
|
|
})
|
|
|
|
ephemeralTarget := EphemeralTarget(opts.Disk, NoFilesystem)
|
|
|
|
targets := []*Target{efiTarget, biosTarget, bootTarget, metaTarget, stateTarget, ephemeralTarget}
|
|
|
|
if !opts.Force {
|
|
for _, target := range targets {
|
|
target.Force = false
|
|
target.Skip = true
|
|
}
|
|
}
|
|
|
|
for _, target := range targets {
|
|
if target == nil {
|
|
continue
|
|
}
|
|
|
|
manifest.Targets[target.Device] = append(manifest.Targets[target.Device], target)
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
// Execute partitions and formats all disks in a manifest.
|
|
func (m *Manifest) Execute() (err error) {
|
|
for dev, targets := range m.Targets {
|
|
if err = m.executeOnDevice(m.Devices[dev], targets); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkMounts verifies that no active mounts in any mount namespace exist for the device.
|
|
//
|
|
//nolint:gocyclo
|
|
func (m *Manifest) checkMounts(device Device) error {
|
|
matches, err := filepath.Glob("/proc/*/mountinfo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, path := range matches {
|
|
path := path
|
|
|
|
if err = func() error {
|
|
var f *os.File
|
|
|
|
f, err = os.Open(path)
|
|
if err != nil {
|
|
// ignore error in case process got removed
|
|
return nil //nolint:nilerr
|
|
}
|
|
|
|
defer f.Close() //nolint:errcheck
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
|
|
if !device.SkipOverlayMountsCheck && fields[len(fields)-2] == "overlay" {
|
|
//nolint:dupword
|
|
// parsing options (last column) in the overlay mount line which looks like:
|
|
// 163 70 0:52 /apid / ro,relatime - overlay overlay rw,lowerdir=/opt,upperdir=/var/system/overlays/opt-diff,workdir=/var/system/overlays/opt-workdir
|
|
|
|
options := strings.Split(fields[len(fields)-1], ",")
|
|
|
|
for _, option := range options {
|
|
parts := strings.SplitN(option, "=", 2)
|
|
if len(parts) == 2 {
|
|
if strings.HasPrefix(parts[1], "/var/") {
|
|
return fmt.Errorf("found overlay mount in %q: %s", path, scanner.Text())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(fields[len(fields)-2], device.Device) {
|
|
return fmt.Errorf("found active mount in %q for %q: %s", path, device.Device, scanner.Text())
|
|
}
|
|
}
|
|
|
|
return f.Close()
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:gocyclo,cyclop
|
|
func (m *Manifest) executeOnDevice(device Device, targets []*Target) (err error) {
|
|
if err = m.checkMounts(device); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = m.preserveContents(device, targets); err != nil {
|
|
return err
|
|
}
|
|
|
|
if device.Zero {
|
|
if err = m.zeroDevice(device); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var bd *blockdevice.BlockDevice
|
|
|
|
if bd, err = blockdevice.Open(device.Device, blockdevice.WithExclusiveLock(true)); err != nil {
|
|
return err
|
|
}
|
|
|
|
//nolint:errcheck
|
|
defer bd.Close()
|
|
|
|
var pt *gpt.GPT
|
|
|
|
created := false
|
|
|
|
pt, err = bd.PartitionTable()
|
|
if err != nil {
|
|
if !errors.Is(err, blockdevice.ErrMissingPartitionTable) {
|
|
return err
|
|
}
|
|
|
|
log.Printf("creating new partition table on %s", device.Device)
|
|
|
|
gptOpts := []gpt.Option{
|
|
gpt.WithMarkMBRBootable(m.LegacyBIOSSupport),
|
|
}
|
|
|
|
if m.PartitionOptions != nil {
|
|
gptOpts = append(gptOpts, gpt.WithPartitionEntriesStartLBA(m.PartitionOptions.PartitionsOffset))
|
|
}
|
|
|
|
pt, err = gpt.New(bd.Device(), gptOpts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("logical/physical block size: %d/%d", pt.Header().LBA.LogicalBlockSize, pt.Header().LBA.PhysicalBlockSize)
|
|
log.Printf("minimum/optimal I/O size: %d/%d", pt.Header().LBA.MinimalIOSize, pt.Header().LBA.OptimalIOSize)
|
|
|
|
if err = pt.Write(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = bd.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if bd, err = blockdevice.Open(device.Device, blockdevice.WithExclusiveLock(true)); err != nil {
|
|
return err
|
|
}
|
|
|
|
defer bd.Close() //nolint:errcheck
|
|
|
|
created = true
|
|
}
|
|
|
|
if !created {
|
|
if device.ResetPartitionTable {
|
|
log.Printf("resetting partition table on %s", device.Device)
|
|
|
|
// TODO: how should it work with zero option above?
|
|
if err = bd.Reset(); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// clean up partitions which are going to be recreated
|
|
keepPartitions := map[string]struct{}{}
|
|
|
|
for _, target := range targets {
|
|
if target.Skip {
|
|
keepPartitions[target.Label] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// make sure all partitions to be skipped already exist
|
|
missingPartitions := map[string]struct{}{}
|
|
|
|
for label := range keepPartitions {
|
|
missingPartitions[label] = struct{}{}
|
|
}
|
|
|
|
for _, part := range pt.Partitions().Items() {
|
|
delete(missingPartitions, part.Name)
|
|
}
|
|
|
|
if len(missingPartitions) > 0 {
|
|
return fmt.Errorf("some partitions to be skipped are missing: %v", missingPartitions)
|
|
}
|
|
|
|
// delete all partitions which are not skipped
|
|
for _, part := range pt.Partitions().Items() {
|
|
if _, ok := keepPartitions[part.Name]; !ok {
|
|
log.Printf("deleting partition %s", part.Name)
|
|
|
|
if err = pt.Delete(part); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = pt.Write(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
pt, err = bd.PartitionTable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, target := range targets {
|
|
if err = target.Partition(pt, i, bd); err != nil {
|
|
return fmt.Errorf("failed to partition device: %w", err)
|
|
}
|
|
}
|
|
|
|
if err = pt.Write(); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, target := range targets {
|
|
target := target
|
|
|
|
err = retry.Constant(time.Minute, retry.WithUnits(100*time.Millisecond)).Retry(func() error {
|
|
e := target.Format()
|
|
if e != nil {
|
|
if strings.Contains(e.Error(), "No such file or directory") {
|
|
// workaround problem with partition device not being visible immediately after partitioning
|
|
return retry.ExpectedError(e)
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to format device: %w", err)
|
|
}
|
|
}
|
|
|
|
return m.restoreContents(targets)
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (m *Manifest) preserveContents(device Device, targets []*Target) (err error) {
|
|
anyPreserveContents := false
|
|
|
|
for _, target := range targets {
|
|
if target.Skip {
|
|
continue
|
|
}
|
|
|
|
if target.PreserveContents {
|
|
anyPreserveContents = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !anyPreserveContents {
|
|
// no target to preserve contents, exit early
|
|
return nil
|
|
}
|
|
|
|
var bd *blockdevice.BlockDevice
|
|
|
|
if bd, err = blockdevice.Open(device.Device); err != nil {
|
|
// failed to open the block device, probably it's damaged?
|
|
log.Printf("warning: skipping preserve contents on %q as block device failed: %s", device.Device, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:errcheck
|
|
defer bd.Close()
|
|
|
|
pt, err := bd.PartitionTable()
|
|
if err != nil {
|
|
log.Printf("warning: skipping preserve contents on %q as partition table failed: %s", device.Device, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, target := range targets {
|
|
if target.Skip {
|
|
continue
|
|
}
|
|
|
|
if !target.PreserveContents {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
sourcePart *gpt.Partition
|
|
fileSystemType partition.FileSystemType
|
|
fnmatchFilters []string
|
|
)
|
|
|
|
sources := append([]PreserveSource{
|
|
{
|
|
Label: target.Label,
|
|
FileSystemType: target.FileSystemType,
|
|
},
|
|
}, target.ExtraPreserveSources...)
|
|
|
|
for _, source := range sources {
|
|
// find matching existing partition table entry
|
|
for _, part := range pt.Partitions().Items() {
|
|
if part.Name == source.Label {
|
|
sourcePart = part
|
|
fileSystemType = source.FileSystemType
|
|
fnmatchFilters = source.FnmatchFilters
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if sourcePart == nil {
|
|
log.Printf("warning: failed to preserve contents of %q on %q, as source partition wasn't found", target.Label, device.Device)
|
|
|
|
continue
|
|
}
|
|
|
|
if err = target.SaveContents(device, sourcePart, fileSystemType, fnmatchFilters); err != nil {
|
|
log.Printf("warning: failed to preserve contents of %q on %q: %s", target.Label, device.Device, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manifest) restoreContents(targets []*Target) error {
|
|
for _, target := range targets {
|
|
if err := target.RestoreContents(); err != nil {
|
|
return fmt.Errorf("error restoring contents for %q: %w", target.Label, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SystemMountpoints returns list of system mountpoints for the manifest.
|
|
func (m *Manifest) SystemMountpoints(opts ...mount.Option) (*mount.Points, error) {
|
|
mountpoints := mount.NewMountPoints()
|
|
|
|
for dev := range m.Targets {
|
|
mp, err := mount.SystemMountPointsForDevice(dev, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
iter := mp.Iter()
|
|
for iter.Next() {
|
|
mountpoints.Set(iter.Key(), iter.Value())
|
|
}
|
|
}
|
|
|
|
return mountpoints, nil
|
|
}
|
|
|
|
// zeroDevice fills the device with zeroes.
|
|
func (m *Manifest) zeroDevice(device Device) (err error) {
|
|
var bd *blockdevice.BlockDevice
|
|
|
|
log.Printf("wiping %q", device.Device)
|
|
|
|
if bd, err = blockdevice.Open(device.Device, blockdevice.WithExclusiveLock(true)); err != nil {
|
|
return err
|
|
}
|
|
|
|
defer bd.Close() //nolint:errcheck
|
|
|
|
var method string
|
|
|
|
if method, err = bd.Wipe(); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("wiped %q with %q", device.Device, method)
|
|
|
|
return bd.Close()
|
|
}
|
|
|
|
func shouldSkipOverlayMountsCheck(sequence runtime.Sequence) (bool, error) {
|
|
var skipOverlayMountsCheck bool
|
|
|
|
_, err := os.Stat("/.dockerenv")
|
|
|
|
switch {
|
|
case err == nil:
|
|
skipOverlayMountsCheck = true
|
|
case os.IsNotExist(err):
|
|
skipOverlayMountsCheck = sequence == runtime.SequenceNoop
|
|
default:
|
|
return false, fmt.Errorf("cannot determine if /.dockerenv exists: %w", err)
|
|
}
|
|
|
|
return skipOverlayMountsCheck, nil
|
|
}
|