talos/cmd/installer/pkg/install/manifest.go
Andrey Smirnov 96aa9638f7
chore: rename talos-systems/talos to siderolabs/talos
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>
2022-11-03 16:50:32 +04:00

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
}