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>
422 lines
9.9 KiB
Go
422 lines
9.9 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 (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/siderolabs/go-blockdevice/blockdevice"
|
|
"github.com/siderolabs/go-blockdevice/blockdevice/partition/gpt"
|
|
"github.com/siderolabs/go-blockdevice/blockdevice/util"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/siderolabs/talos/internal/pkg/mount"
|
|
"github.com/siderolabs/talos/internal/pkg/partition"
|
|
"github.com/siderolabs/talos/pkg/archiver"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
)
|
|
|
|
// Target represents an installation partition.
|
|
//
|
|
//nolint:maligned
|
|
type Target struct {
|
|
*partition.FormatOptions
|
|
Device string
|
|
|
|
LegacyBIOSBootable 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 partition.FileSystemType
|
|
}
|
|
|
|
// NoFilesystem preset to override default filesystem type to none.
|
|
var NoFilesystem = &Target{
|
|
FormatOptions: &partition.FormatOptions{
|
|
FileSystemType: partition.FilesystemTypeNone,
|
|
},
|
|
}
|
|
|
|
// EFITarget builds the default EFI target.
|
|
func EFITarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.EFIPartitionLabel),
|
|
Device: device,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
// BIOSTarget builds the default BIOS target.
|
|
func BIOSTarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.BIOSGrubPartitionLabel),
|
|
Device: device,
|
|
LegacyBIOSBootable: true,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
// BootTarget builds the default boot target.
|
|
func BootTarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.BootPartitionLabel),
|
|
Device: device,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
// MetaTarget builds the default meta target.
|
|
func MetaTarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.MetaPartitionLabel),
|
|
Device: device,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
// StateTarget builds the default state target.
|
|
func StateTarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.StatePartitionLabel),
|
|
Device: device,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
// EphemeralTarget builds the default ephemeral target.
|
|
func EphemeralTarget(device string, extra *Target) *Target {
|
|
target := &Target{
|
|
FormatOptions: partition.NewFormatOptions(constants.EphemeralPartitionLabel),
|
|
Device: device,
|
|
}
|
|
|
|
return target.enhance(extra)
|
|
}
|
|
|
|
func (t *Target) enhance(extra *Target) *Target {
|
|
if extra == nil {
|
|
return t
|
|
}
|
|
|
|
t.Assets = extra.Assets
|
|
t.PreserveContents = extra.PreserveContents
|
|
t.ExtraPreserveSources = extra.ExtraPreserveSources
|
|
t.Skip = extra.Skip
|
|
|
|
if extra.FormatOptions != nil {
|
|
t.FormatOptions.FileSystemType = extra.FormatOptions.FileSystemType
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
func (t *Target) String() string {
|
|
return fmt.Sprintf("%s (%q)", t.PartitionName, t.Label)
|
|
}
|
|
|
|
// Locate existing partition on the disk.
|
|
func (t *Target) Locate(pt *gpt.GPT) (*gpt.Partition, error) {
|
|
for _, part := range pt.Partitions().Items() {
|
|
if part.Name == t.Label {
|
|
var err error
|
|
|
|
t.PartitionName, err = part.Path()
|
|
if err != nil {
|
|
return part, err
|
|
}
|
|
|
|
return part, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// Partition creates a new partition on the specified device.
|
|
func (t *Target) Partition(pt *gpt.GPT, pos int, bd *blockdevice.BlockDevice) (err error) {
|
|
if t.Skip {
|
|
part := pt.Partitions().FindByName(t.Label)
|
|
if part != nil {
|
|
log.Printf("skipped %s (%s) size %d blocks", t.PartitionName, t.Label, part.Length())
|
|
|
|
t.PartitionName, err = part.Path()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
log.Printf("partitioning %s - %s %q\n", t.Device, t.Label, humanize.Bytes(t.Size))
|
|
|
|
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 = part.Path()
|
|
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.
|
|
func (t *Target) Format() error {
|
|
if t.Skip {
|
|
return nil
|
|
}
|
|
|
|
return partition.Format(t.PartitionName, t.FormatOptions)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// GetLabel returns the underlaying partition label.
|
|
func (t *Target) GetLabel() string {
|
|
return t.Label
|
|
}
|
|
|
|
func withTemporaryMounted(partPath string, flags uintptr, fileSystemType partition.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 partition.FileSystemType, fnmatchFilters []string) error {
|
|
partPath, err := util.PartPath(device.Device, int(source.Number))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fileSystemType == partition.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 partition.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 == partition.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)
|
|
})
|
|
}
|