talos/cmd/installer/pkg/install/manifest.go
Andrey Smirnov 29fb7ef07b fix: zero out partitions without filesystems on install
This makes sure Talos won't pick up any potential leftover data on fresh
install. On upgrade contents of META partitions are preserved anyways.

Fixes #2919

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
2020-12-08 04:41:06 -08:00

922 lines
21 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"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/talos-systems/go-blockdevice/blockdevice"
"github.com/talos-systems/go-blockdevice/blockdevice/partition/gpt"
"github.com/talos-systems/go-blockdevice/blockdevice/util"
"github.com/talos-systems/go-retry/retry"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/board"
"github.com/talos-systems/talos/internal/pkg/mount"
"github.com/talos-systems/talos/pkg/archiver"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/makefs"
)
// 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
}
// Device represents device options.
type Device struct {
Device string
ResetPartitionTable bool
Zero bool
}
// Target represents an installation partition.
//
//nolint: golint, maligned
type Target struct {
Device string
Label string
PartitionType PartitionType
FileSystemType FileSystemType
LegacyBIOSBootable bool
Size uint64
Force bool
Assets []*Asset
// Preserve contents of the partition with the same label (if it exists).
PreserveContents bool
// Extra preserved locations (for upgrading from older versions of Talos).
//
// Used only if PreserveContents is true.
ExtraPreserveSources []PreserveSource
// Skip makes manifest skip any actions with the partition (creating, formatting).
//
// Skipped partitions should exist on the disk by the time manifest execution starts.
Skip bool
// set during execution
PartitionName string
Contents *bytes.Buffer
}
// Asset represents a file required by a target.
type Asset struct {
Source string
Destination string
}
// PreserveSource instructs Talos where to look for source files to preserve.
type PreserveSource struct {
Label string
FnmatchFilters []string
FileSystemType FileSystemType
}
// PartitionType in partition table.
type PartitionType = string
// GPT partition types.
//
// TODO: should be moved into the blockdevice library.
const (
EFISystemPartition PartitionType = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
BIOSBootPartition PartitionType = "21686148-6449-6E6F-744E-656564454649"
LinuxFilesystemData PartitionType = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
)
// FileSystemType is used to format partitions.
type FileSystemType = string
// Filesystem types.
const (
FilesystemTypeNone FileSystemType = "none"
FilesystemTypeXFS FileSystemType = "xfs"
FilesystemTypeVFAT FileSystemType = "vfat"
)
// Partition default sizes.
const (
MiB = 1024 * 1024
EFISize = 100 * MiB
BIOSGrubSize = 1 * MiB
BootSize = 300 * MiB
MetaSize = 1 * MiB
StateSize = 100 * MiB
)
// 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{},
}
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)
}
}
manifest.Devices[opts.Disk] = Device{
Device: opts.Disk,
ResetPartitionTable: opts.Force,
Zero: opts.Zero,
}
// 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 := &Target{
Device: opts.Disk,
Label: constants.EFIPartitionLabel,
PartitionType: EFISystemPartition,
FileSystemType: FilesystemTypeVFAT,
Size: EFISize,
Force: true,
}
biosTarget := &Target{
Device: opts.Disk,
Label: constants.BIOSGrubPartitionLabel,
PartitionType: BIOSBootPartition,
FileSystemType: FilesystemTypeNone,
LegacyBIOSBootable: true,
Size: BIOSGrubSize,
Force: true,
}
var bootTarget *Target
if opts.Bootloader {
bootTarget = &Target{
Device: opts.Disk,
Label: constants.BootPartitionLabel,
PartitionType: LinuxFilesystemData,
FileSystemType: FilesystemTypeXFS,
Size: BootSize,
Force: true,
PreserveContents: bootPartitionFound,
Assets: []*Asset{
{
Source: constants.KernelAssetPath,
Destination: filepath.Join(constants.BootMountPoint, label, constants.KernelAsset),
},
{
Source: constants.InitramfsAssetPath,
Destination: filepath.Join(constants.BootMountPoint, label, constants.InitramfsAsset),
},
},
}
}
metaTarget := &Target{
Device: opts.Disk,
Label: constants.MetaPartitionLabel,
PartitionType: LinuxFilesystemData,
FileSystemType: FilesystemTypeNone,
Size: MetaSize,
Force: true,
PreserveContents: bootPartitionFound,
}
stateTarget := &Target{
Device: opts.Disk,
Label: constants.StatePartitionLabel,
PartitionType: LinuxFilesystemData,
FileSystemType: FilesystemTypeXFS,
Size: StateSize,
Force: true,
PreserveContents: bootPartitionFound,
ExtraPreserveSources: []PreserveSource{
{
Label: constants.LegacyBootPartitionLabel,
FileSystemType: FilesystemTypeVFAT,
FnmatchFilters: []string{"config.yaml"},
},
},
}
ephemeralTarget := &Target{
Device: opts.Disk,
Label: constants.EphemeralPartitionLabel,
PartitionType: LinuxFilesystemData,
FileSystemType: FilesystemTypeXFS,
Size: 0,
}
if opts.Force {
ephemeralTarget.Force = true
} else {
ephemeralTarget.Skip = true
stateTarget.Size = 0 // expand previous partition to cover whatever space is available
}
for _, target := range []*Target{efiTarget, biosTarget, bootTarget, metaTarget, stateTarget, ephemeralTarget} {
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.
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
}
defer f.Close() //nolint: errcheck
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 2 {
continue
}
if 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
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); 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{}
if m.PartitionOptions != nil {
gptOpts = append(gptOpts, gpt.WithPartitionEntriesStartLBA(m.PartitionOptions.PartitionsOffset))
}
pt, err = gpt.New(bd.Device(), gptOpts...)
if err != nil {
return err
}
if err = pt.Write(); err != nil {
return err
}
if bd, err = blockdevice.Open(device.Device); err != nil {
return err
}
created = true
}
if !created && 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
}
if err = bd.RereadPartitionTable(); 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
}
if err = bd.RereadPartitionTable(); 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
}
if err = bd.RereadPartitionTable(); err != nil {
log.Printf("failed to re-read partition table on %q: %s, ignoring error...", device.Device, 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 retry.UnexpectedError(e)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to format device: %w", err)
}
}
if err = m.restoreContents(targets); err != nil {
return err
}
return nil
}
//nolint: gocyclo
func (m *Manifest) preserveContents(device Device, targets []*Target) (err error) {
anyPreserveContents := false
for _, target := range targets {
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.PreserveContents {
continue
}
var (
sourcePart *gpt.Partition
fileSystemType 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() (*mount.Points, error) {
mountpoints := mount.NewMountPoints()
for dev := range m.Targets {
mp, err := mount.SystemMountPointsForDevice(dev)
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); 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()
}
// Partition creates a new partition on the specified device.
// nolint: dupl, gocyclo
func (t *Target) Partition(pt *gpt.GPT, pos int, bd *blockdevice.BlockDevice) (err error) {
if t.Skip {
for _, part := range pt.Partitions().Items() {
if part.Name == t.Label {
t.PartitionName, err = util.PartPath(t.Device, int(part.Number))
if err != nil {
return err
}
log.Printf("skipped %s (%s) size %d blocks", t.PartitionName, t.Label, part.Length())
break
}
}
return nil
}
log.Printf("partitioning %s - %s\n", t.Device, t.Label)
opts := []gpt.PartitionOption{
gpt.WithPartitionType(t.PartitionType),
gpt.WithPartitionName(t.Label),
}
if t.Size == 0 {
opts = append(opts, gpt.WithMaximumSize(true))
}
if t.LegacyBIOSBootable {
opts = append(opts, gpt.WithLegacyBIOSBootableAttribute(true))
}
part, err := pt.InsertAt(pos, t.Size, opts...)
if err != nil {
return err
}
t.PartitionName, err = util.PartPath(t.Device, int(part.Number))
if err != nil {
return err
}
log.Printf("created %s (%s) size %d blocks", t.PartitionName, t.Label, part.Length())
return nil
}
// Format creates a filesystem on the device/partition.
//
//nolint: gocyclo
func (t *Target) Format() error {
if t.Skip {
return nil
}
if t.FileSystemType == FilesystemTypeNone {
return t.zeroPartition()
}
log.Printf("formatting partition %q as %q with label %q\n", t.PartitionName, t.FileSystemType, t.Label)
opts := []makefs.Option{makefs.WithForce(t.Force), makefs.WithLabel(t.Label)}
switch t.FileSystemType {
case FilesystemTypeVFAT:
return makefs.VFAT(t.PartitionName, opts...)
case FilesystemTypeXFS:
return makefs.XFS(t.PartitionName, opts...)
default:
return fmt.Errorf("unsupported filesystem type: %q", t.FileSystemType)
}
}
// Save copies the assets to the bootloader partition.
func (t *Target) Save() (err error) {
for _, asset := range t.Assets {
asset := asset
err = func() error {
var (
sourceFile *os.File
destFile *os.File
)
if sourceFile, err = os.Open(asset.Source); err != nil {
return err
}
// nolint: errcheck
defer sourceFile.Close()
if err = os.MkdirAll(filepath.Dir(asset.Destination), os.ModeDir); err != nil {
return err
}
if destFile, err = os.Create(asset.Destination); err != nil {
return err
}
// nolint: errcheck
defer destFile.Close()
log.Printf("copying %s to %s\n", sourceFile.Name(), destFile.Name())
if _, err = io.Copy(destFile, sourceFile); err != nil {
log.Printf("failed to copy %s to %s\n", sourceFile.Name(), destFile.Name())
return err
}
if err = destFile.Close(); err != nil {
log.Printf("failed to close %s", destFile.Name())
return err
}
if err = sourceFile.Close(); err != nil {
log.Printf("failed to close %s", sourceFile.Name())
return err
}
return nil
}()
if err != nil {
return err
}
}
return nil
}
func withTemporaryMounted(partPath string, flags uintptr, fileSystemType FileSystemType, label string, f func(mountPath string) error) error {
mountPath := filepath.Join(constants.SystemPath, "mnt")
mountpoints := mount.NewMountPoints()
mountpoint := mount.NewMountPoint(partPath, mountPath, fileSystemType, unix.MS_NOATIME|flags, "")
mountpoints.Set(label, mountpoint)
if err := mount.Mount(mountpoints); err != nil {
return fmt.Errorf("failed to mount %q: %w", partPath, err)
}
defer func() {
if err := mount.Unmount(mountpoints); err != nil {
log.Printf("failed to unmount: %s", err)
}
}()
return f(mountPath)
}
// SaveContents saves contents of partition to the target (in-memory).
func (t *Target) SaveContents(device Device, source *gpt.Partition, fileSystemType FileSystemType, fnmatchFilters []string) error {
partPath, err := util.PartPath(device.Device, int(source.Number))
if err != nil {
return err
}
if fileSystemType == FilesystemTypeNone {
err = t.saveRawContents(partPath)
} else {
err = t.saveFilesystemContents(partPath, fileSystemType, fnmatchFilters)
}
if err != nil {
t.Contents = nil
return err
}
log.Printf("preserved contents of %q: %d bytes", t.Label, t.Contents.Len())
return nil
}
func (t *Target) saveRawContents(partPath string) error {
src, err := os.Open(partPath)
if err != nil {
return fmt.Errorf("error opening source partition: %q", err)
}
defer src.Close() //nolint: errcheck
t.Contents = bytes.NewBuffer(nil)
zw := gzip.NewWriter(t.Contents)
defer zw.Close() //nolint: errcheck
_, err = io.Copy(zw, src)
if err != nil {
return fmt.Errorf("error copying partition %q contents: %w", partPath, err)
}
return src.Close()
}
func (t *Target) saveFilesystemContents(partPath string, fileSystemType FileSystemType, fnmatchFilters []string) error {
t.Contents = bytes.NewBuffer(nil)
return withTemporaryMounted(partPath, unix.MS_RDONLY, fileSystemType, t.Label, func(mountPath string) error {
return archiver.TarGz(context.TODO(), mountPath, t.Contents, archiver.WithFnmatchPatterns(fnmatchFilters...))
})
}
// RestoreContents restores previously saved contents to the disk.
func (t *Target) RestoreContents() error {
if t.Contents == nil {
return nil
}
var err error
if t.FileSystemType == FilesystemTypeNone {
err = t.restoreRawContents()
} else {
err = t.restoreFilesystemContents()
}
t.Contents = nil
if err != nil {
return err
}
log.Printf("restored contents of %q", t.Label)
return nil
}
func (t *Target) restoreRawContents() error {
dst, err := os.OpenFile(t.PartitionName, os.O_WRONLY, 0)
if err != nil {
return fmt.Errorf("error opening source partition: %q", err)
}
defer dst.Close() //nolint: errcheck
zr, err := gzip.NewReader(t.Contents)
if err != nil {
return err
}
_, err = io.Copy(dst, zr)
if err != nil {
return fmt.Errorf("error restoring partition %q contents: %w", t.PartitionName, err)
}
return dst.Close()
}
func (t *Target) restoreFilesystemContents() error {
return withTemporaryMounted(t.PartitionName, 0, t.FileSystemType, t.Label, func(mountPath string) error {
return archiver.UntarGz(context.TODO(), t.Contents, mountPath)
})
}
// zeroPartition fills the partition with zeroes.
func (t *Target) zeroPartition() (err error) {
log.Printf("zeroing out %q", t.PartitionName)
zeroes, err := os.Open("/dev/zero")
if err != nil {
return err
}
defer zeroes.Close() //nolint: errcheck
part, err := os.OpenFile(t.PartitionName, os.O_WRONLY, 0)
if err != nil {
return err
}
defer part.Close() //nolint: errcheck
_, err = io.CopyN(part, zeroes, int64(t.Size))
return err
}