mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-22 15:11:10 +02:00
579 lines
16 KiB
Go
579 lines
16 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 (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice/filesystem/vfat"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice/filesystem/xfs"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice/probe"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice/table"
|
|
"github.com/talos-systems/talos/internal/pkg/blockdevice/table/gpt/partition"
|
|
"github.com/talos-systems/talos/internal/pkg/constants"
|
|
"github.com/talos-systems/talos/internal/pkg/version"
|
|
"github.com/talos-systems/talos/pkg/userdata"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
// DefaultSizeRootDevice is the default size of the root partition.
|
|
// TODO(andrewrynhard): We should inspect the tarball's uncompressed size and dynamically set the root partition's size.
|
|
DefaultSizeRootDevice = 2048 * 1000 * 1000
|
|
|
|
// DefaultSizeDataDevice is the default size of the data partition.
|
|
DefaultSizeDataDevice = 1024 * 1000 * 1000
|
|
|
|
// DefaultSizeBootDevice is the default size of the boot partition.
|
|
// TODO(andrewrynhard): We should inspect the sizes of the artifacts and dynamically set the boot partition's size.
|
|
DefaultSizeBootDevice = 512 * 1000 * 1000
|
|
)
|
|
|
|
var (
|
|
// DefaultURLBase is the base URL for all default artifacts.
|
|
// TODO(andrewrynhard): We need to setup infrastructure for publishing artifacts and not depend on GitHub.
|
|
DefaultURLBase = "https://github.com/talos-systems/talos/releases/download/" + version.Tag
|
|
|
|
// DefaultRootfsURL is the URL to the rootfs.
|
|
DefaultRootfsURL = DefaultURLBase + "/rootfs.tar.gz"
|
|
|
|
// DefaultKernelURL is the URL to the kernel.
|
|
DefaultKernelURL = DefaultURLBase + "/vmlinuz"
|
|
|
|
// DefaultInitramfsURL is the URL to the initramfs.
|
|
DefaultInitramfsURL = DefaultURLBase + "/initramfs.xz"
|
|
)
|
|
|
|
// Prepare handles setting/consolidating/defaulting userdata pieces specific to
|
|
// installation
|
|
// TODO: See if this would be more appropriate in userdata
|
|
// nolint: dupl, gocyclo
|
|
func Prepare(data *userdata.UserData) (err error) {
|
|
if data.Install == nil {
|
|
return nil
|
|
}
|
|
|
|
var exists bool
|
|
if exists, err = Exists(data.Install.Boot.InstallDevice.Device); err != nil {
|
|
return err
|
|
}
|
|
|
|
if exists {
|
|
log.Println("found existing installation, skipping prepare step")
|
|
return nil
|
|
}
|
|
|
|
// Verify that the target device(s) can satisify the requested options.
|
|
|
|
if err = VerifyRootDevice(data); err != nil {
|
|
return errors.Wrap(err, "failed to prepare root device")
|
|
}
|
|
if err = VerifyDataDevice(data); err != nil {
|
|
return errors.Wrap(err, "failed to prepare data device")
|
|
}
|
|
if err = VerifyBootDevice(data); err != nil {
|
|
return errors.Wrap(err, "failed to prepare boot device")
|
|
}
|
|
|
|
manifest := NewManifest(data)
|
|
|
|
if data.Install.Wipe {
|
|
if err = WipeDevices(manifest); err != nil {
|
|
return errors.Wrap(err, "failed to wipe device(s)")
|
|
}
|
|
}
|
|
|
|
// Create and format all partitions.
|
|
|
|
if err = ExecuteManifest(data, manifest); err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ExecuteManifest partitions and formats all disks in a manifest.
|
|
func ExecuteManifest(data *userdata.UserData, manifest *Manifest) (err error) {
|
|
for dev, targets := range manifest.Targets {
|
|
var bd *blockdevice.BlockDevice
|
|
if bd, err = blockdevice.Open(dev, blockdevice.WithNewGPT(data.Install.Force)); err != nil {
|
|
return err
|
|
}
|
|
// nolint: errcheck
|
|
defer bd.Close()
|
|
|
|
for _, target := range targets {
|
|
if err = target.Partition(bd); err != nil {
|
|
return errors.Wrap(err, "failed to partition device")
|
|
}
|
|
}
|
|
|
|
if err = bd.RereadPartitionTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, target := range targets {
|
|
if err = target.Format(); err != nil {
|
|
return errors.Wrap(err, "failed to format device")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyRootDevice verifies the supplied root device options.
|
|
func VerifyRootDevice(data *userdata.UserData) (err error) {
|
|
if data.Install == nil || data.Install.Root == nil {
|
|
return errors.New("a root device is required")
|
|
}
|
|
|
|
if data.Install.Root.Device == "" {
|
|
return errors.New("a root device is required")
|
|
}
|
|
|
|
if data.Install.Root.Size == 0 {
|
|
data.Install.Root.Size = DefaultSizeRootDevice
|
|
}
|
|
|
|
if data.Install.Root.Rootfs == "" {
|
|
data.Install.Root.Rootfs = DefaultRootfsURL
|
|
}
|
|
|
|
if !data.Install.Force {
|
|
if err = VerifyDiskAvailability(constants.CurrentRootPartitionLabel()); err != nil {
|
|
return errors.Wrap(err, "failed to verify disk availability")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyDataDevice verifies the supplied data device options.
|
|
func VerifyDataDevice(data *userdata.UserData) (err error) {
|
|
// Set data device to root device if not specified
|
|
if data.Install.Data == nil {
|
|
data.Install.Data = &userdata.InstallDevice{}
|
|
}
|
|
|
|
if data.Install.Data.Device == "" {
|
|
// We can safely assume root device is defined at this point
|
|
// because VerifyRootDevice should have been called first in
|
|
// in the chain
|
|
data.Install.Data.Device = data.Install.Root.Device
|
|
}
|
|
|
|
if data.Install.Data.Size == 0 {
|
|
data.Install.Data.Size = DefaultSizeDataDevice
|
|
}
|
|
|
|
if !data.Install.Force {
|
|
if err = VerifyDiskAvailability(constants.DataPartitionLabel); err != nil {
|
|
return errors.Wrap(err, "failed to verify disk availability")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyBootDevice verifies the supplied boot device options.
|
|
func VerifyBootDevice(data *userdata.UserData) (err error) {
|
|
if data.Install.Boot == nil {
|
|
return nil
|
|
}
|
|
|
|
if data.Install.Boot.Device == "" {
|
|
// We can safely assume root device is defined at this point
|
|
// because VerifyRootDevice should have been called first in
|
|
// in the chain
|
|
data.Install.Boot.Device = data.Install.Root.Device
|
|
}
|
|
|
|
if data.Install.Boot.Size == 0 {
|
|
data.Install.Boot.Size = DefaultSizeBootDevice
|
|
}
|
|
|
|
if data.Install.Boot.Kernel == "" {
|
|
data.Install.Boot.Kernel = DefaultKernelURL
|
|
}
|
|
|
|
if data.Install.Boot.Initramfs == "" {
|
|
data.Install.Boot.Initramfs = DefaultInitramfsURL
|
|
}
|
|
|
|
if !data.Install.Force {
|
|
if err = VerifyDiskAvailability(constants.BootPartitionLabel); err != nil {
|
|
return errors.Wrap(err, "failed to verify disk availability")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyDiskAvailability verifies that no filesystems currently exist with
|
|
// the labels used by the OS.
|
|
func VerifyDiskAvailability(label string) (err error) {
|
|
var dev *probe.ProbedBlockDevice
|
|
if dev, err = probe.GetDevWithFileSystemLabel(label); err != nil {
|
|
// We return here because we only care if we can discover the
|
|
// device successfully and confirm that the disk is not in use.
|
|
// TODO(andrewrynhard): We should return a custom error type here
|
|
// that we can use to confirm the device was not found.
|
|
return nil
|
|
}
|
|
if dev.SuperBlock != nil {
|
|
return errors.Errorf("target install device %s is not empty, found existing %s file system", label, dev.SuperBlock.Type())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WipeDevices writes zeros to each block device in the preparation manifest.
|
|
func WipeDevices(manifest *Manifest) (err error) {
|
|
var zero *os.File
|
|
if zero, err = os.Open("/dev/zero"); err != nil {
|
|
return err
|
|
}
|
|
|
|
for dev := range manifest.Targets {
|
|
var f *os.File
|
|
if f, err = os.OpenFile(dev, os.O_RDWR, os.ModeDevice); err != nil {
|
|
return err
|
|
}
|
|
var size uint64
|
|
if _, _, ret := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.BLKGETSIZE64, uintptr(unsafe.Pointer(&size))); ret != 0 {
|
|
return errors.Errorf("failed to got block device size: %v", ret)
|
|
}
|
|
if _, err = io.CopyN(f, zero, int64(size)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = f.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = zero.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewManifest initializes and returns a Manifest.
|
|
func NewManifest(data *userdata.UserData) (manifest *Manifest) {
|
|
manifest = &Manifest{
|
|
Targets: map[string][]*Target{},
|
|
}
|
|
|
|
// Initialize any slices we need. Note that a boot paritition is not
|
|
// required.
|
|
|
|
if manifest.Targets[data.Install.Root.Device] == nil {
|
|
manifest.Targets[data.Install.Root.Device] = []*Target{}
|
|
}
|
|
if manifest.Targets[data.Install.Data.Device] == nil {
|
|
manifest.Targets[data.Install.Data.Device] = []*Target{}
|
|
}
|
|
|
|
var bootTarget *Target
|
|
if data.Install.Boot != nil {
|
|
bootTarget = &Target{
|
|
Device: data.Install.Boot.Device,
|
|
Label: constants.BootPartitionLabel,
|
|
Size: data.Install.Boot.Size,
|
|
Force: data.Install.Force,
|
|
Test: false,
|
|
Assets: []*Asset{
|
|
{
|
|
Source: data.Install.Boot.Kernel,
|
|
Destination: filepath.Join("/", constants.CurrentRootPartitionLabel(), filepath.Base(data.Install.Boot.Kernel)),
|
|
},
|
|
{
|
|
Source: data.Install.Boot.Initramfs,
|
|
Destination: filepath.Join("/", constants.CurrentRootPartitionLabel(), filepath.Base(data.Install.Boot.Initramfs)),
|
|
},
|
|
},
|
|
MountPoint: path.Join(constants.NewRoot, constants.BootMountPoint),
|
|
}
|
|
}
|
|
|
|
rootATarget := &Target{
|
|
Device: data.Install.Root.Device,
|
|
Label: constants.RootAPartitionLabel,
|
|
Size: data.Install.Root.Size,
|
|
Force: data.Install.Force,
|
|
Test: false,
|
|
Assets: []*Asset{
|
|
{
|
|
Source: data.Install.Root.Rootfs,
|
|
Destination: filepath.Base(data.Install.Root.Rootfs),
|
|
},
|
|
},
|
|
MountPoint: path.Join(constants.NewRoot, constants.RootMountPoint),
|
|
}
|
|
|
|
rootBTarget := &Target{
|
|
Device: data.Install.Root.Device,
|
|
Label: constants.RootBPartitionLabel,
|
|
Size: data.Install.Root.Size,
|
|
Force: data.Install.Force,
|
|
Test: false,
|
|
MountPoint: path.Join(constants.NewRoot, constants.RootMountPoint),
|
|
}
|
|
|
|
dataTarget := &Target{
|
|
Device: data.Install.Data.Device,
|
|
Label: constants.DataPartitionLabel,
|
|
Size: data.Install.Data.Size,
|
|
Force: data.Install.Force,
|
|
Test: false,
|
|
MountPoint: path.Join(constants.NewRoot, constants.DataMountPoint),
|
|
}
|
|
|
|
for _, target := range []*Target{bootTarget, rootATarget, rootBTarget, dataTarget} {
|
|
if target == nil {
|
|
continue
|
|
}
|
|
manifest.Targets[target.Device] = append(manifest.Targets[target.Device], target)
|
|
}
|
|
|
|
for _, extra := range data.Install.ExtraDevices {
|
|
if manifest.Targets[extra.Device] == nil {
|
|
manifest.Targets[extra.Device] = []*Target{}
|
|
}
|
|
|
|
for _, part := range extra.Partitions {
|
|
extraTarget := &Target{
|
|
Device: extra.Device,
|
|
Size: part.Size,
|
|
Force: data.Install.Force,
|
|
Test: false,
|
|
}
|
|
|
|
manifest.Targets[extra.Device] = append(manifest.Targets[extra.Device], extraTarget)
|
|
}
|
|
}
|
|
|
|
return manifest
|
|
}
|
|
|
|
// Manifest represents the instructions for preparing all block devices
|
|
// for an installation.
|
|
type Manifest struct {
|
|
Targets map[string][]*Target
|
|
}
|
|
|
|
// Target represents an installation partition.
|
|
type Target struct {
|
|
Label string
|
|
MountPoint string
|
|
Device string
|
|
FileSystemType string
|
|
PartitionName string
|
|
Size uint
|
|
Force bool
|
|
Test bool
|
|
Assets []*Asset
|
|
BlockDevice *blockdevice.BlockDevice
|
|
}
|
|
|
|
// Asset represents a file required by a target.
|
|
type Asset struct {
|
|
Source string
|
|
Destination string
|
|
}
|
|
|
|
// Partition creates a new partition on the specified device
|
|
// nolint: dupl, gocyclo
|
|
func (t *Target) Partition(bd *blockdevice.BlockDevice) (err error) {
|
|
log.Printf("partitioning %s - %s\n", t.Device, t.Label)
|
|
|
|
var pt table.PartitionTable
|
|
if pt, err = bd.PartitionTable(true); err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := []interface{}{partition.WithPartitionTest(t.Test)}
|
|
|
|
switch t.Label {
|
|
case constants.BootPartitionLabel:
|
|
// EFI System Partition
|
|
typeID := "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
|
|
opts = append(opts, partition.WithPartitionType(typeID), partition.WithPartitionName(t.Label), partition.WithLegacyBIOSBootableAttribute(true))
|
|
case constants.CurrentRootPartitionLabel():
|
|
// Root Partition
|
|
var typeID string
|
|
switch runtime.GOARCH {
|
|
case "386":
|
|
typeID = "44479540-F297-41B2-9AF7-D131D5F0458A"
|
|
case "amd64":
|
|
typeID = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709"
|
|
default:
|
|
return errors.Errorf("%s", "unsupported cpu architecture")
|
|
}
|
|
opts = append(opts, partition.WithPartitionType(typeID), partition.WithPartitionName(t.Label))
|
|
case constants.DataPartitionLabel:
|
|
// Data Partition
|
|
typeID := "AF3DC60F-8384-7247-8E79-3D69D8477DE4"
|
|
opts = append(opts, partition.WithPartitionType(typeID), partition.WithPartitionName(t.Label))
|
|
default:
|
|
typeID := "AF3DC60F-8384-7247-8E79-3D69D8477DE4"
|
|
opts = append(opts, partition.WithPartitionType(typeID))
|
|
}
|
|
|
|
part, err := pt.Add(uint64(t.Size), opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = pt.Write(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(andrewrynhard): We should really have a custom type that has all
|
|
// the methods we need. This switch statement shows up in some form in
|
|
// multiple places.
|
|
switch dev := t.Device; {
|
|
case strings.HasPrefix(dev, "/dev/nvme"):
|
|
case strings.HasPrefix(dev, "/dev/loop"):
|
|
t.PartitionName = t.Device + "p" + strconv.Itoa(int(part.No()))
|
|
default:
|
|
t.PartitionName = t.Device + strconv.Itoa(int(part.No()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Format creates a xfs filesystem on the device/partition
|
|
func (t *Target) Format() error {
|
|
log.Printf("formatting partition %s - %s\n", t.PartitionName, t.Label)
|
|
if t.Label == constants.BootPartitionLabel {
|
|
return vfat.MakeFS(t.PartitionName, vfat.WithLabel(t.Label))
|
|
}
|
|
opts := []xfs.Option{xfs.WithForce(t.Force)}
|
|
if t.Label != "" {
|
|
opts = append(opts, xfs.WithLabel(t.Label))
|
|
}
|
|
return xfs.MakeFS(t.PartitionName, opts...)
|
|
}
|
|
|
|
// Install handles downloading the necessary assets and extracting them to
|
|
// the appropriate locations
|
|
// nolint: gocyclo
|
|
func (t *Target) Install() error {
|
|
// Download and extract all artifacts.
|
|
var sourceFile *os.File
|
|
var destFile *os.File
|
|
var err error
|
|
|
|
if err = os.MkdirAll(t.MountPoint, os.ModeDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Extract artifact if necessary, otherwise place at root of partition/filesystem
|
|
for _, asset := range t.Assets {
|
|
var u *url.URL
|
|
if u, err = url.Parse(asset.Source); err != nil {
|
|
return err
|
|
}
|
|
|
|
sourceFile = nil
|
|
destFile = nil
|
|
|
|
// Handle fetching of asset
|
|
switch u.Scheme {
|
|
case "http":
|
|
fallthrough
|
|
case "https":
|
|
log.Printf("downloading %s\n", asset.Source)
|
|
dest := filepath.Join(t.MountPoint, asset.Destination)
|
|
sourceFile, err = download(u.String(), dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "file":
|
|
source := u.Path
|
|
dest := filepath.Join(t.MountPoint, asset.Destination)
|
|
|
|
log.Printf("copying %s to %s\n", asset.Source, dest)
|
|
sourceFile, err = os.Open(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = os.MkdirAll(filepath.Dir(dest), 0700); err != nil {
|
|
return err
|
|
}
|
|
|
|
destFile, err = os.Create(dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported path scheme, got %s but supported %s", u.Scheme, "https://|http://|file://")
|
|
}
|
|
|
|
// Handle extraction/Installation
|
|
switch {
|
|
// tar
|
|
case strings.HasSuffix(sourceFile.Name(), ".tar") || strings.HasSuffix(sourceFile.Name(), ".tar.gz"):
|
|
log.Printf("extracting %s to %s\n", sourceFile.Name(), t.MountPoint)
|
|
|
|
err = untar(sourceFile, t.MountPoint)
|
|
if err != nil {
|
|
log.Printf("failed to extract %s to %s\n", sourceFile.Name(), t.MountPoint)
|
|
return err
|
|
}
|
|
|
|
if err = sourceFile.Close(); err != nil {
|
|
log.Printf("failed to close %s", sourceFile.Name())
|
|
return err
|
|
}
|
|
|
|
if err = os.Remove(sourceFile.Name()); err != nil {
|
|
log.Printf("failed to remove %s", sourceFile.Name())
|
|
return err
|
|
}
|
|
// single file
|
|
case strings.HasPrefix(sourceFile.Name(), "/") && destFile != nil:
|
|
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
|
|
}
|
|
default:
|
|
if err = sourceFile.Close(); err != nil {
|
|
log.Printf("failed to close %s", sourceFile.Name())
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|