talos/internal/pkg/install/prepare.go
Andrew Rynhard e18b5086a9
chore: update org to new name (#480)
Signed-off-by: Andrew Rynhard <andrew@andrewrynhard.com>
2019-04-03 18:29:21 -07:00

377 lines
10 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 (
"io"
"log"
"os"
"runtime"
"strconv"
"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
}
// 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.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.RootPartitionLabel); 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) {
if data.Install.Data.Device == "" {
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 {
if data.Install.Boot.Device == "" {
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.
if manifest.Targets[data.Install.Boot.Device] == nil {
manifest.Targets[data.Install.Boot.Device] = []*Target{}
}
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{}
}
bootTarget := &Target{
Device: data.Install.Boot.Device,
Label: constants.BootPartitionLabel,
Size: data.Install.Boot.Size,
Force: data.Install.Force,
Test: false,
MountBase: "/tmp",
}
rootTarget := &Target{
Device: data.Install.Root.Device,
Label: constants.RootPartitionLabel,
Size: data.Install.Root.Size,
Force: data.Install.Force,
Test: false,
MountBase: "/tmp",
}
dataTarget := &Target{
Device: data.Install.Data.Device,
Label: constants.DataPartitionLabel,
Size: data.Install.Data.Size,
Force: data.Install.Force,
Test: false,
MountBase: "/tmp",
}
for _, target := range []*Target{bootTarget, rootTarget, dataTarget} {
manifest.Targets[target.Device] = append(manifest.Targets[target.Device], target)
}
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
MountBase string
Device string
FileSystemType string
PartitionName string
Size uint
Force bool
Test bool
BlockDevice *blockdevice.BlockDevice
}
// Partition creates a new partition on the specified device
// nolint: dupl, gocyclo
func (t *Target) Partition(bd *blockdevice.BlockDevice) (err error) {
var (
typeID string
legacyBIOSBootable bool
)
log.Printf("partitioning %s - %s\n", t.Device, t.Label)
var pt table.PartitionTable
if pt, err = bd.PartitionTable(true); err != nil {
return err
}
switch t.Label {
case constants.BootPartitionLabel:
// EFI System Partition
typeID = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
legacyBIOSBootable = true
case constants.RootPartitionLabel:
// Root Partition
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")
}
case constants.DataPartitionLabel:
// Data Partition
typeID = "AF3DC60F-8384-7247-8E79-3D69D8477DE4"
default:
return errors.Errorf("%s", "unknown partition label")
}
part, err := pt.Add(
uint64(t.Size),
partition.WithPartitionType(typeID),
partition.WithPartitionName(t.Label),
partition.WithLegacyBIOSBootableAttribute(legacyBIOSBootable),
partition.WithPartitionTest(t.Test),
)
if err != nil {
return err
}
if err = pt.Write(); err != nil {
return err
}
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))
}
return xfs.MakeFS(t.PartitionName, xfs.WithLabel(t.Label), xfs.WithForce(t.Force))
}