diff --git a/cmd/osctl/cmd/install_linux.go b/cmd/osctl/cmd/install_linux.go index b29bd2c21..dd5400f70 100644 --- a/cmd/osctl/cmd/install_linux.go +++ b/cmd/osctl/cmd/install_linux.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" // "github.com/talos-systems/talos/cmd/osctl/internal/userdata" "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/install" + "github.com/talos-systems/talos/internal/pkg/installer" "github.com/talos-systems/talos/internal/pkg/kernel" "github.com/talos-systems/talos/internal/pkg/version" "github.com/talos-systems/talos/pkg/userdata" @@ -55,14 +55,6 @@ var installCmd = &cobra.Command{ } } - if err = install.Prepare(data); err != nil { - log.Fatal(err) - } - - if err = install.Mount(data); err != nil { - log.Fatal(err) - } - cmdline := kernel.NewDefaultCmdline() cmdline.Append("initrd", filepath.Join("/", "default", "initramfs.xz")) cmdline.Append(constants.KernelParamPlatform, platform) @@ -71,7 +63,8 @@ var installCmd = &cobra.Command{ log.Fatal(err) } - if err = install.Install(cmdline.String(), data); err != nil { + i := installer.NewInstaller(cmdline, data) + if err = i.Install(); err != nil { log.Fatal(err) } diff --git a/internal/app/init/internal/mount/mount.go b/internal/app/init/internal/mount/mount.go deleted file mode 100644 index 59533ddb2..000000000 --- a/internal/app/init/internal/mount/mount.go +++ /dev/null @@ -1,199 +0,0 @@ -/* 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 mount - -import ( - "os" - - "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/mount" - "golang.org/x/sys/unix" - "gopkg.in/freddierice/go-losetup.v1" -) - -// Initializer represents the early boot initialization control. -type Initializer struct { - prefix string - - special *mount.Points -} - -// NewInitializer initializes and returns an Initializer struct. -func NewInitializer(prefix string) (initializer *Initializer, err error) { - special := mount.NewMountPoints() - special.Set("dev", mount.NewMountPoint("devtmpfs", "/dev", "devtmpfs", unix.MS_NOSUID, "mode=0755")) - special.Set("proc", mount.NewMountPoint("proc", "/proc", "proc", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")) - special.Set("sys", mount.NewMountPoint("sysfs", "/sys", "sysfs", 0, "")) - special.Set("run", mount.NewMountPoint("tmpfs", "/run", "tmpfs", 0, "")) - special.Set("tmp", mount.NewMountPoint("tmpfs", "/tmp", "tmpfs", 0, "")) - - initializer = &Initializer{ - prefix: prefix, - special: special, - } - - return initializer, nil -} - -// Special returns the special devices. -func (i *Initializer) Special() *mount.Points { - return i.special -} - -// InitSpecial initializes and mounts the special devices in the early boot -// tasks. -func (i *Initializer) InitSpecial() (err error) { - iter := i.special.Iter() - for iter.Next() { - if err = mount.WithRetry(iter.Value()); err != nil { - return errors.Errorf("error initializing special device at %s: %v", iter.Value().Target(), err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// Rootfs initializes and mounts the OS owned block devices in the early boot -// tasks. -func (i *Initializer) Rootfs() (err error) { - var dev losetup.Device - dev, err = losetup.Attach("/"+constants.RootfsAsset, 0, true) - if err != nil { - return err - } - - m := mount.NewMountPoint(dev.Path(), "/", "squashfs", unix.MS_RDONLY, "") - if err = mount.WithRetry(m, mount.WithPrefix(i.prefix), mount.WithReadOnly(true), mount.WithShared(true)); err != nil { - return errors.Wrap(err, "failed to mount squashfs") - } - - return nil -} - -// MoveSpecial moves the special device mount points to the new root. -func (i *Initializer) MoveSpecial() (err error) { - iter := i.special.Iter() - for iter.Next() { - mountpoint := mount.NewMountPoint(iter.Value().Target(), iter.Value().Target(), "", unix.MS_MOVE, "") - if err = mount.WithRetry(mountpoint, mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error moving mount point %s: %v", iter.Value().Target(), err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - if err = mount.WithRetry(mount.NewMountPoint("tmpfs", "/dev/shm", "tmpfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, ""), mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error mounting mount point %s: %v", "/dev/shm", err) - } - - if err = mount.WithRetry(mount.NewMountPoint("devpts", "/dev/pts", "devpts", unix.MS_NOSUID|unix.MS_NOEXEC, "ptmxmode=000,mode=620,gid=5"), mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error mounting mount point %s: %v", "/dev/pts", err) - } - - return nil -} - -// Switch moves the root to a specified directory. See -// https://github.com/karelzak/util-linux/blob/master/sys-utils/switch_root.c. -// nolint: gocyclo -func (i *Initializer) Switch() (err error) { - if err = i.MoveSpecial(); err != nil { - return errors.Wrap(err, "error moving special devices") - } - - if err = unix.Chdir(i.prefix); err != nil { - return errors.Wrapf(err, "error changing working directory to %s", i.prefix) - } - - var old *os.File - if old, err = os.Open("/"); err != nil { - return errors.Wrap(err, "error opening /") - } - // nolint: errcheck - defer old.Close() - - if err = unix.Mount(i.prefix, "/", "", unix.MS_MOVE, ""); err != nil { - return errors.Wrap(err, "error moving /") - } - - if err = unix.Chroot("."); err != nil { - return errors.Wrap(err, "error chroot") - } - - if err = recursiveDelete(int(old.Fd())); err != nil { - return errors.Wrap(err, "error deleting initramfs") - } - - // Note that /sbin/init is machined. We call it init since this is the - // convention. - if err = unix.Exec("/sbin/init", []string{"/sbin/init"}, []string{}); err != nil { - return errors.Wrap(err, "error executing /sbin/init") - } - - return nil -} - -func recursiveDelete(fd int) error { - parentDev, err := getDev(fd) - if err != nil { - return err - } - - dir := os.NewFile(uintptr(fd), "__ignored__") - // nolint: errcheck - defer dir.Close() - names, err := dir.Readdirnames(-1) - if err != nil { - return err - } - - for _, name := range names { - if err := recusiveDeleteInner(fd, parentDev, name); err != nil { - return err - } - } - return nil -} - -func recusiveDeleteInner(parentFd int, parentDev uint64, childName string) error { - childFd, err := unix.Openat(parentFd, childName, unix.O_DIRECTORY|unix.O_NOFOLLOW, unix.O_RDWR) - if err != nil { - if err := unix.Unlinkat(parentFd, childName, 0); err != nil { - return err - } - } else { - // nolint: errcheck - defer unix.Close(childFd) - - if childFdDev, err := getDev(childFd); err != nil { - return err - } else if childFdDev != parentDev { - return nil - } - - if err := recursiveDelete(childFd); err != nil { - return err - } - if err := unix.Unlinkat(parentFd, childName, unix.AT_REMOVEDIR); err != nil { - return err - } - } - return nil -} - -func getDev(fd int) (dev uint64, err error) { - var stat unix.Stat_t - - if err := unix.Fstat(fd, &stat); err != nil { - return 0, err - } - - return stat.Dev, nil -} diff --git a/internal/app/init/main.go b/internal/app/init/main.go index 36a80f750..2a70b062b 100644 --- a/internal/app/init/main.go +++ b/internal/app/init/main.go @@ -6,22 +6,27 @@ package main import ( "log" + "time" "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/app/init/internal/mount" "github.com/talos-systems/talos/internal/pkg/constants" "github.com/talos-systems/talos/internal/pkg/kmsg" + "github.com/talos-systems/talos/internal/pkg/mount/manager" + "github.com/talos-systems/talos/internal/pkg/mount/manager/squashfs" + "github.com/talos-systems/talos/internal/pkg/mount/manager/virtual" + "github.com/talos-systems/talos/internal/pkg/mount/switchroot" + "golang.org/x/sys/unix" ) // nolint: gocyclo -func initram() (err error) { - var initializer *mount.Initializer - if initializer, err = mount.NewInitializer(constants.NewRoot); err != nil { +func run() (err error) { + // Mount the virtual devices. + mountpoints, err := virtual.MountPoints() + if err != nil { return err } - - // Mount the special devices. - if err = initializer.InitSpecial(); err != nil { + virtual := manager.NewManager(mountpoints) + if err = virtual.MountAll(); err != nil { return err } @@ -31,23 +36,43 @@ func initram() (err error) { return err } - // Perform the equivalent of switch_root. + // Mount the rootfs. log.Println("mounting the rootfs") - if err = initializer.Rootfs(); err != nil { + mountpoints, err = squashfs.MountPoints(constants.NewRoot) + if err != nil { + return err + } + squashfs := manager.NewManager(mountpoints) + if err = squashfs.MountAll(); err != nil { return err } - // Perform the equivalent of switch_root. + // Switch into the new rootfs. log.Println("entering the rootfs") - if err = initializer.Switch(); err != nil { + if err = switchroot.Switch(constants.NewRoot, virtual); err != nil { return err } return nil } +func recovery() { + if r := recover(); r != nil { + log.Printf("recovered from: %+v\n", r) + for i := 10; i >= 0; i-- { + log.Printf("rebooting in %d seconds\n", i) + time.Sleep(1 * time.Second) + } + } + + // nolint: errcheck + unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART) +} + func main() { - if err := initram(); err != nil { + defer recovery() + + if err := run(); err != nil { panic(errors.Wrap(err, "early boot failed")) } diff --git a/internal/app/machined/internal/mount/cgroups/cgroups.go b/internal/app/machined/internal/mount/cgroups/cgroups.go deleted file mode 100644 index 1a6ed6f2b..000000000 --- a/internal/app/machined/internal/mount/cgroups/cgroups.go +++ /dev/null @@ -1,67 +0,0 @@ -/* 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 cgroups - -import ( - "io/ioutil" - "os" - "path" - "strconv" - - "github.com/pkg/errors" - - "golang.org/x/sys/unix" -) - -const ( - memoryCgroup = "memory" - memoryUseHierarchy = "memory.use_hierarchy" - memoryUseHierarchyPermissions = os.FileMode(400) -) - -var ( - memoryUseHierarchyContents = []byte(strconv.Itoa(1)) -) - -// Mount creates the cgroup mount points. -func Mount() error { - target := "/sys/fs/cgroup" - if err := os.MkdirAll(target, os.ModeDir); err != nil { - return errors.Errorf("failed to create %s: %+v", target, err) - } - if err := unix.Mount("tmpfs", target, "tmpfs", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "mode=755"); err != nil { - return errors.Errorf("failed to mount %s: %+v", target, err) - } - - cgroups := []string{ - "blkio", - "cpu", - "cpuacct", - "cpuset", - "devices", - "freezer", - "hugetlb", - "memory", - "net_cls", - "net_prio", - "perf_event", - "pids", - } - for _, c := range cgroups { - p := path.Join("/sys/fs/cgroup", c) - if err := os.MkdirAll(p, os.ModeDir); err != nil { - return errors.Errorf("failed to create %s: %+v", p, err) - } - if err := unix.Mount(c, p, "cgroup", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, c); err != nil { - return errors.Errorf("failed to mount %s: %+v", p, err) - } - } - - // See https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt - target = path.Join("/sys/fs/cgroup", memoryCgroup, memoryUseHierarchy) - err := ioutil.WriteFile(target, memoryUseHierarchyContents, memoryUseHierarchyPermissions) - - return err -} diff --git a/internal/app/machined/internal/mount/mount.go b/internal/app/machined/internal/mount/mount.go deleted file mode 100644 index 2898f0fd1..000000000 --- a/internal/app/machined/internal/mount/mount.go +++ /dev/null @@ -1,198 +0,0 @@ -/* 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 mount - -import ( - "fmt" - "log" - "path" - - "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/pkg/blockdevice" - "github.com/talos-systems/talos/internal/pkg/blockdevice/filesystem/xfs" - "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" - gptpartition "github.com/talos-systems/talos/internal/pkg/blockdevice/table/gpt/partition" - "github.com/talos-systems/talos/internal/pkg/blockdevice/util" - "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/mount" - "github.com/talos-systems/talos/pkg/userdata" - "golang.org/x/sys/unix" -) - -// Initializer represents the early boot initialization control. -type Initializer struct { - prefix string - - owned *mount.Points -} - -// NewInitializer initializes and returns an Initializer struct. -func NewInitializer(prefix string) (initializer *Initializer, err error) { - initializer = &Initializer{ - prefix: prefix, - } - - return initializer, nil -} - -// Owned returns the OS owned block devices. -func (i *Initializer) Owned() *mount.Points { - return i.owned -} - -// InitOwned initializes and mounts the OS owned block devices in the early boot -// tasks. -func (i *Initializer) InitOwned() (err error) { - var owned *mount.Points - if owned, err = mountpoints(); err != nil { - return errors.Errorf("error initializing owned block devices: %v", err) - } - i.owned = owned - if mountpoint, ok := i.owned.Get(constants.DataPartitionLabel); ok { - if err = repair(mountpoint); err != nil { - return errors.Errorf("error fixing data partition: %v", err) - } - } - - iter := i.owned.Iter() - for iter.Next() { - if err = mount.WithRetry(iter.Value(), mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error mounting partitions: %v", err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - if mountpoint, ok := i.owned.Get(constants.DataPartitionLabel); ok { - // NB: The XFS partition MUST be mounted, or this will fail. - log.Printf("growing the %s partition", constants.DataPartitionLabel) - if err = xfs.GrowFS(path.Join(i.prefix, mountpoint.Target())); err != nil { - return errors.Errorf("error growing data partition file system: %v", err) - } - } - - return nil -} - -// ExtraDevices mounts the extra devices. -func ExtraDevices(data *userdata.UserData) (err error) { - if data.Install == nil || data.Install.ExtraDevices == nil { - return nil - } - for _, extra := range data.Install.ExtraDevices { - for i, part := range extra.Partitions { - devname := fmt.Sprintf("%s%d", extra.Device, i+1) - mountpoint := mount.NewMountPoint(devname, part.MountPoint, "xfs", unix.MS_NOATIME, "") - if err = mount.WithRetry(mountpoint); err != nil { - return errors.Errorf("failed to mount %s at %s: %v", devname, part.MountPoint, err) - } - } - } - - return nil -} - -// MountOwned mounts the OS owned block devices. -func (i *Initializer) MountOwned() (err error) { - iter := i.owned.Iter() - for iter.Next() { - if err = mount.WithRetry(iter.Value(), mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error mounting partitions: %v", err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// UnmountOwned unmounts the OS owned block devices. -func (i *Initializer) UnmountOwned() (err error) { - iter := i.owned.IterRev() - for iter.Next() { - if err = mount.UnWithRetry(iter.Value(), mount.WithPrefix(i.prefix)); err != nil { - return errors.Errorf("error mounting partitions: %v", err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// nolint: dupl -func mountpoints() (mountpoints *mount.Points, err error) { - mountpoints = mount.NewMountPoints() - for _, name := range []string{constants.DataPartitionLabel, constants.BootPartitionLabel} { - var target string - switch name { - case constants.DataPartitionLabel: - target = constants.DataMountPoint - case constants.BootPartitionLabel: - target = constants.BootMountPoint - } - - var dev *probe.ProbedBlockDevice - if dev, err = probe.GetDevWithFileSystemLabel(name); err != nil { - if name == constants.BootPartitionLabel { - // A bootloader is not always required. - log.Println("WARNING: no ESP partition was found") - continue - } - return nil, errors.Errorf("failed to find device with label %s: %v", name, err) - } - - mountpoint := mount.NewMountPoint(dev.Path, target, dev.SuperBlock.Type(), unix.MS_NOATIME, "") - - mountpoints.Set(name, mountpoint) - } - - return mountpoints, nil -} - -func repair(mountpoint *mount.Point) (err error) { - var devname string - if devname, err = util.DevnameFromPartname(mountpoint.Source()); err != nil { - return err - } - bd, err := blockdevice.Open("/dev/" + devname) - if err != nil { - return errors.Errorf("error opening block device %q: %v", devname, err) - } - // nolint: errcheck - defer bd.Close() - - pt, err := bd.PartitionTable(true) - if err != nil { - return err - } - - if err := pt.Repair(); err != nil { - return err - } - - for _, partition := range pt.Partitions() { - if partition.(*gptpartition.Partition).Name == constants.DataPartitionLabel { - if err := pt.Resize(partition); err != nil { - return err - } - } - } - - if err := pt.Write(); err != nil { - return err - } - - // Rereading the partition table requires that all partitions be unmounted - // or it will fail with EBUSY. - if err := bd.RereadPartitionTable(); err != nil { - return err - } - - return nil -} diff --git a/internal/app/machined/internal/phase/install/install.go b/internal/app/machined/internal/phase/install/install.go deleted file mode 100644 index b826954b2..000000000 --- a/internal/app/machined/internal/phase/install/install.go +++ /dev/null @@ -1,59 +0,0 @@ -/* 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 ( - "log" - - "github.com/talos-systems/talos/internal/app/machined/internal/mount" - "github.com/talos-systems/talos/internal/app/machined/internal/phase" - "github.com/talos-systems/talos/internal/app/machined/internal/platform" - "github.com/talos-systems/talos/internal/app/machined/internal/runtime" - "github.com/talos-systems/talos/pkg/userdata" -) - -// Install represents the Install task. -type Install struct{} - -// NewInstallTask initializes and returns an Install task. -func NewInstallTask() phase.Task { - return &Install{} -} - -// RuntimeFunc returns the runtime function. -func (task *Install) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { - switch mode { - case runtime.Standard: - return task.runtime - default: - return nil - } -} - -func (task *Install) runtime(platform platform.Platform, data *userdata.UserData) (err error) { - // Perform any tasks required by a particular platform. - log.Printf("performing platform specific tasks") - if err = platform.Prepare(data); err != nil { - return err - } - - var initializer *mount.Initializer - if initializer, err = mount.NewInitializer(""); err != nil { - return err - } - - // Mount the owned partitions. - log.Printf("mounting the owned partitions") - if err = initializer.InitOwned(); err != nil { - return err - } - - // Install handles additional system setup - if err = platform.Install(data); err != nil { - return err - } - - return nil -} diff --git a/internal/app/machined/internal/phase/platform/platform.go b/internal/app/machined/internal/phase/platform/platform.go new file mode 100644 index 000000000..4d30a7076 --- /dev/null +++ b/internal/app/machined/internal/phase/platform/platform.go @@ -0,0 +1,33 @@ +/* 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 platform + +import ( + "github.com/talos-systems/talos/internal/app/machined/internal/phase" + "github.com/talos-systems/talos/internal/app/machined/internal/platform" + "github.com/talos-systems/talos/internal/app/machined/internal/runtime" + "github.com/talos-systems/talos/pkg/userdata" +) + +// Platform represents the Platform task. +type Platform struct{} + +// NewPlatformTask initializes and returns an Platform task. +func NewPlatformTask() phase.Task { + return &Platform{} +} + +// RuntimeFunc returns the runtime function. +func (task *Platform) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { + return task.runtime +} + +func (task *Platform) runtime(platform platform.Platform, data *userdata.UserData) (err error) { + if err = platform.Initialize(data); err != nil { + return err + } + + return nil +} diff --git a/internal/pkg/install/install_test.go b/internal/app/machined/internal/phase/platform/platform_test.go similarity index 94% rename from internal/pkg/install/install_test.go rename to internal/app/machined/internal/phase/platform/platform_test.go index eee2c96cc..38c0e0ff9 100644 --- a/internal/pkg/install/install_test.go +++ b/internal/app/machined/internal/phase/platform/platform_test.go @@ -2,7 +2,7 @@ * 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_test +package platform_test import "testing" diff --git a/internal/app/machined/internal/phase/rootfs/etc/etc.go b/internal/app/machined/internal/phase/rootfs/etc/etc.go index 7d3c73a6c..6c4e3d716 100644 --- a/internal/app/machined/internal/phase/rootfs/etc/etc.go +++ b/internal/app/machined/internal/phase/rootfs/etc/etc.go @@ -42,7 +42,7 @@ BUG_REPORT_URL="https://github.com/talos-systems/talos/issues" // Hosts renders a valid /etc/hosts file and writes it to disk. func Hosts() (err error) { var h *string - if h = kernel.Cmdline().Get(constants.KernelParamHostname).First(); h != nil { + if h = kernel.ProcCmdline().Get(constants.KernelParamHostname).First(); h != nil { if err = unix.Sethostname([]byte(*h)); err != nil { return err } diff --git a/internal/app/machined/internal/phase/rootfs/mount_cgroups.go b/internal/app/machined/internal/phase/rootfs/mount_cgroups.go index 3e2574232..fac2b2713 100644 --- a/internal/app/machined/internal/phase/rootfs/mount_cgroups.go +++ b/internal/app/machined/internal/phase/rootfs/mount_cgroups.go @@ -5,14 +5,32 @@ package rootfs import ( + // "github.com/pkg/errors" + "io/ioutil" + "os" + "path" + "strconv" + "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/app/machined/internal/mount/cgroups" "github.com/talos-systems/talos/internal/app/machined/internal/phase" "github.com/talos-systems/talos/internal/app/machined/internal/platform" "github.com/talos-systems/talos/internal/app/machined/internal/runtime" + "github.com/talos-systems/talos/internal/pkg/mount" + "github.com/talos-systems/talos/internal/pkg/mount/manager" + "github.com/talos-systems/talos/internal/pkg/mount/manager/cgroups" "github.com/talos-systems/talos/pkg/userdata" ) +const ( + memoryCgroup = "memory" + memoryUseHierarchy = "memory.use_hierarchy" + memoryUseHierarchyPermissions = os.FileMode(400) +) + +var ( + memoryUseHierarchyContents = []byte(strconv.Itoa(1)) +) + // MountCgroups represents the MountCgroups task. type MountCgroups struct{} @@ -32,8 +50,21 @@ func (task *MountCgroups) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { } func (task *MountCgroups) runtime(platform platform.Platform, data *userdata.UserData) (err error) { - if err = cgroups.Mount(); err != nil { - return errors.Wrap(err, "error mounting cgroups") + var mountpoints *mount.Points + mountpoints, err = cgroups.MountPoints() + if err != nil { + return err + } + + m := manager.NewManager(mountpoints) + if err = m.MountAll(); err != nil { + return err + } + + // See https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt + target := path.Join("/sys/fs/cgroup", memoryCgroup, memoryUseHierarchy) + if err = ioutil.WriteFile(target, memoryUseHierarchyContents, memoryUseHierarchyPermissions); err != nil { + return errors.Wrap(err, "failed to enable memory hierarchy support") } return nil diff --git a/internal/app/machined/internal/phase/rootfs/mount_sub_devices.go b/internal/app/machined/internal/phase/rootfs/mount_sub_devices.go new file mode 100644 index 000000000..2e094a3e7 --- /dev/null +++ b/internal/app/machined/internal/phase/rootfs/mount_sub_devices.go @@ -0,0 +1,50 @@ +/* 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 rootfs + +import ( + // "github.com/pkg/errors" + + "github.com/talos-systems/talos/internal/app/machined/internal/phase" + "github.com/talos-systems/talos/internal/app/machined/internal/platform" + "github.com/talos-systems/talos/internal/app/machined/internal/runtime" + "github.com/talos-systems/talos/internal/pkg/mount" + "github.com/talos-systems/talos/internal/pkg/mount/manager" + "github.com/talos-systems/talos/internal/pkg/mount/manager/virtual" + "github.com/talos-systems/talos/pkg/userdata" +) + +// MountSubDevices represents the MountSubDevices task. +type MountSubDevices struct{} + +// NewMountSubDevicesTask initializes and returns an MountSubDevices task. +func NewMountSubDevicesTask() phase.Task { + return &MountSubDevices{} +} + +// RuntimeFunc returns the runtime function. +func (task *MountSubDevices) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { + switch mode { + case runtime.Standard: + return task.runtime + default: + return nil + } +} + +func (task *MountSubDevices) runtime(platform platform.Platform, data *userdata.UserData) (err error) { + var mountpoints *mount.Points + mountpoints, err = virtual.SubMountPoints() + if err != nil { + return err + } + + m := manager.NewManager(mountpoints) + if err = m.MountAll(); err != nil { + return err + } + + return nil +} diff --git a/internal/app/machined/internal/phase/sysctls/sysctls.go b/internal/app/machined/internal/phase/sysctls/sysctls.go index 191b9b1be..eca1b7901 100644 --- a/internal/app/machined/internal/phase/sysctls/sysctls.go +++ b/internal/app/machined/internal/phase/sysctls/sysctls.go @@ -8,7 +8,7 @@ import ( "github.com/talos-systems/talos/internal/app/machined/internal/phase" "github.com/talos-systems/talos/internal/app/machined/internal/platform" "github.com/talos-systems/talos/internal/app/machined/internal/runtime" - "github.com/talos-systems/talos/internal/pkg/proc" + "github.com/talos-systems/talos/internal/pkg/kernel/sysctl" "github.com/talos-systems/talos/pkg/userdata" ) @@ -26,5 +26,5 @@ func (task *Sysctls) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { } func (task *Sysctls) runtime(platform platform.Platform, data *userdata.UserData) (err error) { - return proc.WriteSystemProperty(&proc.SystemProperty{Key: "net.ipv4.ip_forward", Value: "1"}) + return sysctl.WriteSystemProperty(&sysctl.SystemProperty{Key: "net.ipv4.ip_forward", Value: "1"}) } diff --git a/internal/app/machined/internal/phase/userdata/extra_devices.go b/internal/app/machined/internal/phase/userdata/extra_devices.go index de93e172f..b65548a83 100644 --- a/internal/app/machined/internal/phase/userdata/extra_devices.go +++ b/internal/app/machined/internal/phase/userdata/extra_devices.go @@ -5,11 +5,15 @@ package userdata import ( - "github.com/talos-systems/talos/internal/app/machined/internal/mount" + "fmt" + "github.com/talos-systems/talos/internal/app/machined/internal/phase" "github.com/talos-systems/talos/internal/app/machined/internal/platform" "github.com/talos-systems/talos/internal/app/machined/internal/runtime" + "github.com/talos-systems/talos/internal/pkg/mount" + "github.com/talos-systems/talos/internal/pkg/mount/manager" "github.com/talos-systems/talos/pkg/userdata" + "golang.org/x/sys/unix" ) // ExtraDevices represents the ExtraDevices task. @@ -26,8 +30,20 @@ func (task *ExtraDevices) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { } func (task *ExtraDevices) runtime(platform platform.Platform, data *userdata.UserData) (err error) { - // Mount the extra devices. - if err = mount.ExtraDevices(data); err != nil { + if data.Install == nil || data.Install.ExtraDevices == nil { + return nil + } + + mountpoints := mount.NewMountPoints() + for _, extra := range data.Install.ExtraDevices { + for i, part := range extra.Partitions { + devname := fmt.Sprintf("%s%d", extra.Device, i+1) + mountpoints.Set(devname, mount.NewMountPoint(devname, part.MountPoint, "xfs", unix.MS_NOATIME, "")) + } + } + + extras := manager.NewManager(mountpoints) + if err = extras.MountAll(); err != nil { return err } diff --git a/internal/app/machined/internal/platform/baremetal/baremetal.go b/internal/app/machined/internal/platform/baremetal/baremetal.go index 60af11c67..ab633985a 100644 --- a/internal/app/machined/internal/platform/baremetal/baremetal.go +++ b/internal/app/machined/internal/platform/baremetal/baremetal.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/install" + "github.com/talos-systems/talos/internal/pkg/installer" "github.com/talos-systems/talos/internal/pkg/kernel" "github.com/talos-systems/talos/pkg/userdata" @@ -37,7 +37,7 @@ func (b *BareMetal) Name() string { // UserData implements the platform.Platform interface. func (b *BareMetal) UserData() (data *userdata.UserData, err error) { var option *string - if option = kernel.Cmdline().Get(constants.KernelParamUserData).First(); option == nil { + if option = kernel.ProcCmdline().Get(constants.KernelParamUserData).First(); option == nil { return data, errors.Errorf("no user data option was found") } @@ -71,17 +71,12 @@ func (b *BareMetal) UserData() (data *userdata.UserData, err error) { return userdata.Download(*option) } -// Prepare implements the platform.Platform interface. -func (b *BareMetal) Prepare(data *userdata.UserData) (err error) { - return install.Prepare(data) -} - -// Install provides the functionality to install talos by downloading the +// Initialize provides the functionality to install talos by downloading the // required artifacts and writing them to a target device. // nolint: dupl -func (b *BareMetal) Install(data *userdata.UserData) (err error) { +func (b *BareMetal) Initialize(data *userdata.UserData) (err error) { var endpoint *string - if endpoint = kernel.Cmdline().Get(constants.KernelParamUserData).First(); endpoint == nil { + if endpoint = kernel.ProcCmdline().Get(constants.KernelParamUserData).First(); endpoint == nil { return errors.Errorf("failed to find %s in kernel parameters", constants.KernelParamUserData) } cmdline := kernel.NewDefaultCmdline() @@ -93,7 +88,8 @@ func (b *BareMetal) Install(data *userdata.UserData) (err error) { return err } - if err = install.Install(cmdline.String(), data); err != nil { + i := installer.NewInstaller(cmdline, data) + if err = i.Install(); err != nil { return errors.Wrap(err, "failed to install") } diff --git a/internal/app/machined/internal/platform/cloud/aws/aws.go b/internal/app/machined/internal/platform/cloud/aws/aws.go index 06c76a5a3..86fe65fd7 100644 --- a/internal/app/machined/internal/platform/cloud/aws/aws.go +++ b/internal/app/machined/internal/platform/cloud/aws/aws.go @@ -121,9 +121,9 @@ func (a *AWS) UserData() (*userdata.UserData, error) { return userdata.Download(AWSUserDataEndpoint) } -// Prepare implements the platform.Platform interface and handles initial host preparation. -func (a *AWS) Prepare(data *userdata.UserData) (err error) { - return nil +// Initialize implements the platform.Platform interface and handles additional system setup. +func (a *AWS) Initialize(data *userdata.UserData) (err error) { + return hostname() } func hostname() (err error) { @@ -149,8 +149,3 @@ func hostname() (err error) { return nil } - -// Install implements the platform.Platform interface and handles additional system setup. -func (a *AWS) Install(data *userdata.UserData) (err error) { - return hostname() -} diff --git a/internal/app/machined/internal/platform/cloud/azure/azure.go b/internal/app/machined/internal/platform/cloud/azure/azure.go index 216fe7c72..83a1361db 100644 --- a/internal/app/machined/internal/platform/cloud/azure/azure.go +++ b/internal/app/machined/internal/platform/cloud/azure/azure.go @@ -37,11 +37,6 @@ func (a *Azure) UserData() (*userdata.UserData, error) { return userdata.Download(AzureUserDataEndpoint, userdata.WithHeaders(map[string]string{"Metadata": "true"}), userdata.WithFormat("base64")) } -// Prepare implements the platform.Platform interface and handles initial host preparation. -func (a *Azure) Prepare(data *userdata.UserData) (err error) { - return nil -} - func hostname() (err error) { // TODO get this sorted; assuming we need to set appropriate headers @@ -72,7 +67,7 @@ func hostname() (err error) { */ } -// Install implements the platform.Platform interface and handles additional system setup. -func (a *Azure) Install(data *userdata.UserData) (err error) { +// Initialize implements the platform.Platform interface and handles additional system setup. +func (a *Azure) Initialize(data *userdata.UserData) (err error) { return hostname() } diff --git a/internal/app/machined/internal/platform/cloud/googlecloud/googlecloud.go b/internal/app/machined/internal/platform/cloud/googlecloud/googlecloud.go index 5402987f2..3a004d0d4 100644 --- a/internal/app/machined/internal/platform/cloud/googlecloud/googlecloud.go +++ b/internal/app/machined/internal/platform/cloud/googlecloud/googlecloud.go @@ -46,12 +46,7 @@ func (gc *GoogleCloud) UserData() (data *userdata.UserData, err error) { return ud, nil } -// Prepare implements the platform.Platform interface and handles initial host preparation. -func (gc *GoogleCloud) Prepare(data *userdata.UserData) (err error) { - return nil -} - -// Install implements the platform.Platform interface and handles additional system setup. -func (gc *GoogleCloud) Install(data *userdata.UserData) (err error) { +// Initialize implements the platform.Platform interface and handles additional system setup. +func (gc *GoogleCloud) Initialize(data *userdata.UserData) (err error) { return nil } diff --git a/internal/app/machined/internal/platform/cloud/packet/packet.go b/internal/app/machined/internal/platform/cloud/packet/packet.go index ed9d0ee34..4534318c0 100644 --- a/internal/app/machined/internal/platform/cloud/packet/packet.go +++ b/internal/app/machined/internal/platform/cloud/packet/packet.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/install" + "github.com/talos-systems/talos/internal/pkg/installer" "github.com/talos-systems/talos/internal/pkg/kernel" "github.com/talos-systems/talos/pkg/userdata" ) @@ -32,19 +32,14 @@ func (p *Packet) UserData() (data *userdata.UserData, err error) { return userdata.Download(PacketUserDataEndpoint) } -// Prepare implements the platform.Platform interface. -func (p *Packet) Prepare(data *userdata.UserData) (err error) { - return install.Prepare(data) -} - -// Install provides the functionality to install talos by downloading the -// required artifacts and writing them to a target device. +// Initialize implements the platform.Platform interface. // nolint: dupl -func (p *Packet) Install(data *userdata.UserData) (err error) { +func (p *Packet) Initialize(data *userdata.UserData) (err error) { var endpoint *string - if endpoint = kernel.Cmdline().Get(constants.KernelParamUserData).First(); endpoint == nil { + if endpoint = kernel.ProcCmdline().Get(constants.KernelParamUserData).First(); endpoint == nil { return errors.Errorf("failed to find %s in kernel parameters", constants.KernelParamUserData) } + cmdline := kernel.NewDefaultCmdline() cmdline.Append("initrd", filepath.Join("/", "default", "initramfs.xz")) cmdline.Append(constants.KernelParamPlatform, "packet") @@ -54,7 +49,8 @@ func (p *Packet) Install(data *userdata.UserData) (err error) { return err } - if err = install.Install(cmdline.String(), data); err != nil { + i := installer.NewInstaller(cmdline, data) + if err = i.Install(); err != nil { return errors.Wrap(err, "failed to install") } diff --git a/internal/app/machined/internal/platform/cloud/vmware/vmware.go b/internal/app/machined/internal/platform/cloud/vmware/vmware.go index 555214e37..33cead05b 100644 --- a/internal/app/machined/internal/platform/cloud/vmware/vmware.go +++ b/internal/app/machined/internal/platform/cloud/vmware/vmware.go @@ -28,7 +28,7 @@ func (vmw *VMware) Name() string { // UserData implements the platform.Platform interface. func (vmw *VMware) UserData() (data *userdata.UserData, err error) { var option *string - if option = kernel.Cmdline().Get(constants.KernelParamUserData).First(); option == nil { + if option = kernel.ProcCmdline().Get(constants.KernelParamUserData).First(); option == nil { return data, fmt.Errorf("no user data option was found") } @@ -65,12 +65,7 @@ func (vmw *VMware) UserData() (data *userdata.UserData, err error) { return data, nil } -// Prepare implements the platform.Platform interface and handles initial host preparation. -func (vmw *VMware) Prepare(data *userdata.UserData) (err error) { - return nil -} - -// Install implements the platform.Platform interface and handles additional system setup. -func (vmw *VMware) Install(data *userdata.UserData) (err error) { +// Initialize implements the platform.Platform interface and handles additional system setup. +func (vmw *VMware) Initialize(data *userdata.UserData) (err error) { return nil } diff --git a/internal/app/machined/internal/platform/container/container.go b/internal/app/machined/internal/platform/container/container.go index de4d4e59c..6881d2c27 100644 --- a/internal/app/machined/internal/platform/container/container.go +++ b/internal/app/machined/internal/platform/container/container.go @@ -40,12 +40,7 @@ func (c *Container) UserData() (data *userdata.UserData, err error) { return data, nil } -// Prepare implements the platform.Platform interface. -func (c *Container) Prepare(data *userdata.UserData) (err error) { - return nil -} - -// Install implements the platform.Platform interface. -func (c *Container) Install(data *userdata.UserData) error { +// Initialize implements the platform.Platform interface. +func (c *Container) Initialize(data *userdata.UserData) error { return nil } diff --git a/internal/app/machined/internal/platform/iso/iso.go b/internal/app/machined/internal/platform/iso/iso.go index 438121953..72bd2f744 100644 --- a/internal/app/machined/internal/platform/iso/iso.go +++ b/internal/app/machined/internal/platform/iso/iso.go @@ -14,9 +14,8 @@ import ( "github.com/pkg/errors" "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/install" + "github.com/talos-systems/talos/internal/pkg/installer" "github.com/talos-systems/talos/internal/pkg/kernel" - "github.com/talos-systems/talos/internal/pkg/mount" "github.com/talos-systems/talos/pkg/crypto/x509" "github.com/talos-systems/talos/pkg/userdata" "golang.org/x/sys/unix" @@ -62,21 +61,21 @@ func (i *ISO) UserData() (data *userdata.UserData, err error) { return data, nil } -// Prepare implements the platform.Platform interface. -func (i *ISO) Prepare(data *userdata.UserData) (err error) { +// Initialize implements the platform.Platform interface. +func (i *ISO) Initialize(data *userdata.UserData) (err error) { var dev *probe.ProbedBlockDevice dev, err = probe.GetDevWithFileSystemLabel(constants.ISOFilesystemLabel) if err != nil { return errors.Errorf("failed to find %s iso: %v", constants.ISOFilesystemLabel, err) } - mountpoint := mount.NewMountPoint(dev.Path, "/tmp", dev.SuperBlock.Type(), unix.MS_RDONLY, "") - if err = mount.WithRetry(mountpoint); err != nil { + if err = unix.Mount(dev.Path, "/tmp", dev.SuperBlock.Type(), unix.MS_RDONLY, ""); err != nil { return err } for _, f := range []string{"/tmp/usr/install/vmlinuz", "/tmp/usr/install/initramfs.xz"} { - source, err := ioutil.ReadFile(f) + var source []byte + source, err = ioutil.ReadFile(f) if err != nil { return err } @@ -85,11 +84,6 @@ func (i *ISO) Prepare(data *userdata.UserData) (err error) { } } - return install.Prepare(data) -} - -// Install implements the platform.Platform interface. -func (i *ISO) Install(data *userdata.UserData) error { reader := bufio.NewReader(os.Stdin) fmt.Print("Talos configuration URL: ") endpoint, err := reader.ReadString('\n') @@ -102,7 +96,8 @@ func (i *ISO) Install(data *userdata.UserData) error { cmdline.Append(constants.KernelParamPlatform, "bare-metal") cmdline.Append(constants.KernelParamUserData, endpoint) - if err = install.Install(cmdline.String(), data); err != nil { + inst := installer.NewInstaller(cmdline, data) + if err = inst.Install(); err != nil { return errors.Wrap(err, "failed to install") } diff --git a/internal/app/machined/internal/platform/platform.go b/internal/app/machined/internal/platform/platform.go index b25f25e51..565e76c4b 100644 --- a/internal/app/machined/internal/platform/platform.go +++ b/internal/app/machined/internal/platform/platform.go @@ -25,8 +25,7 @@ import ( type Platform interface { Name() string UserData() (*userdata.UserData, error) - Prepare(*userdata.UserData) error - Install(*userdata.UserData) error + Initialize(*userdata.UserData) error } // NewPlatform is a helper func for discovering the current platform. @@ -34,7 +33,7 @@ type Platform interface { // nolint: gocyclo func NewPlatform() (p Platform, err error) { var platform string - if p := kernel.Cmdline().Get(constants.KernelParamPlatform).First(); p != nil { + if p := kernel.ProcCmdline().Get(constants.KernelParamPlatform).First(); p != nil { platform = *p } diff --git a/internal/app/machined/main.go b/internal/app/machined/main.go index cf408eee4..00c2bda68 100644 --- a/internal/app/machined/main.go +++ b/internal/app/machined/main.go @@ -11,9 +11,9 @@ import ( "github.com/pkg/errors" "github.com/talos-systems/talos/internal/app/machined/internal/phase" - apiptask "github.com/talos-systems/talos/internal/app/machined/internal/phase/api" - "github.com/talos-systems/talos/internal/app/machined/internal/phase/install" + "github.com/talos-systems/talos/internal/app/machined/internal/phase/api" "github.com/talos-systems/talos/internal/app/machined/internal/phase/network" + "github.com/talos-systems/talos/internal/app/machined/internal/phase/platform" "github.com/talos-systems/talos/internal/app/machined/internal/phase/rootfs" "github.com/talos-systems/talos/internal/app/machined/internal/phase/security" "github.com/talos-systems/talos/internal/app/machined/internal/phase/services" @@ -45,6 +45,7 @@ func run() (err error) { security.NewSecurityTask(), rootfs.NewSystemDirectoryTask(), rootfs.NewMountCgroupsTask(), + rootfs.NewMountSubDevicesTask(), sysctls.NewSysctlsTask(), ), phase.NewPhase( @@ -68,11 +69,11 @@ func run() (err error) { userdatatask.NewExtraFilesTask(), ), phase.NewPhase( - "installation", - install.NewInstallTask(), + "platform tasks", + platform.NewPlatformTask(), ), phase.NewPhase( - "overlay", + "overlay mounts", rootfs.NewMountOverlayTask(), ), phase.NewPhase( @@ -85,7 +86,7 @@ func run() (err error) { ), phase.NewPhase( "service setup", - apiptask.NewAPITask(), + api.NewAPITask(), services.NewServicesTask(), ), ) diff --git a/internal/pkg/install/detect.go b/internal/pkg/install/detect.go deleted file mode 100644 index c09a4df5b..000000000 --- a/internal/pkg/install/detect.go +++ /dev/null @@ -1,52 +0,0 @@ -/* 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 ( - "log" - "os" - "path/filepath" - - "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" - "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/mount" -) - -// Exists checks if Talos has already been installed to a block device. -// It works by searching for a filesystem with a well known label, and then, -// if the filesystem exists, checking for the existence of a file. -func Exists(devpath string) (bool, error) { - var ( - err error - dev *probe.ProbedBlockDevice - ) - - if dev, err = probe.DevForFileSystemLabel(devpath, constants.BootPartitionLabel); err == nil { - // nolint: errcheck - defer dev.Close() - if dev.SuperBlock != nil { - mountpoint := mount.NewMountPoint(dev.Path, "/tmp", dev.SuperBlock.Type(), 0, "") - if err = mount.WithRetry(mountpoint); err != nil { - return false, err - } - defer func() { - if err = mount.UnWithRetry(mountpoint); err != nil { - log.Printf("WARNING: failed to unmount %s from /tmp", dev.Path) - } - }() - _, err = os.Stat(filepath.Join("tmp", "installed")) - switch { - case err == nil: - return true, nil - case os.IsNotExist(err): - return false, nil - default: - return false, err - } - } - } - - return false, nil -} diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go deleted file mode 100644 index 55877e3a9..000000000 --- a/internal/pkg/install/install.go +++ /dev/null @@ -1,205 +0,0 @@ -/* 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 ( - "archive/tar" - "compress/gzip" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/pkg/blockdevice/bootloader/syslinux" - "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/pkg/userdata" -) - -// Install fetches the necessary data locations and copies or extracts -// to the target locations -// nolint: gocyclo -func Install(args string, 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 install step") - return nil - } - - manifest := NewManifest(data) - for _, targets := range manifest.Targets { - for _, target := range targets { - // Handle any pre-setup work that's required - switch target.Label { - case constants.BootPartitionLabel: - // Install/Update the bootloader. - if err = syslinux.Prepare(target.Device); err != nil { - return err - } - case constants.DataPartitionLabel: - // Do nothing - continue - } - - // Handles the download and extraction of assets - if err = target.Install(); err != nil { - return err - } - } - } - - if data.Install.Boot == nil { - return nil - } - - syslinuxcfg := &syslinux.Cfg{ - Default: "default", - Labels: []*syslinux.Label{ - { - Root: "default", - Kernel: filepath.Join("/", "default", filepath.Base(data.Install.Boot.Kernel)), - Initrd: filepath.Join("/", "default", filepath.Base(data.Install.Boot.Initramfs)), - Append: args, - }, - }, - } - if err = syslinux.Install(filepath.Join(constants.NewRoot, constants.BootMountPoint), syslinuxcfg); err != nil { - return err - } - if err = ioutil.WriteFile(filepath.Join(constants.NewRoot, constants.BootMountPoint, "installed"), []byte{}, 0400); err != nil { - return err - } - - return nil -} - -// Simple extract function -// nolint: gocyclo, dupl -func untar(tarball *os.File, dst string) error { - - var input io.Reader - var err error - - if strings.HasSuffix(tarball.Name(), ".tar.gz") { - input, err = gzip.NewReader(tarball) - if err != nil { - return err - } - - // nolint: errcheck - defer input.(*gzip.Reader).Close() - } else { - input = tarball - } - - tr := tar.NewReader(input) - - for { - var header *tar.Header - - header, err = tr.Next() - - switch { - case err == io.EOF: - err = nil - return err - case err != nil: - return err - case header == nil: - continue - } - - // the target location where the dir/file should be created - target := filepath.Join(dst, header.Name) - - // May need to add in support for these - /* - // Type '1' to '6' are header-only flags and may not have a data body. - TypeLink = '1' // Hard link - TypeSymlink = '2' // Symbolic link - TypeChar = '3' // Character device node - TypeBlock = '4' // Block device node - TypeDir = '5' // Directory - TypeFifo = '6' // FIFO node - */ - switch header.Typeflag { - case tar.TypeDir: - if err = os.MkdirAll(target, 0755); err != nil { - return err - } - case tar.TypeReg: - var downloadedFileput *os.File - - downloadedFileput, err = os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - return err - } - - if _, err = io.Copy(downloadedFileput, tr); err != nil { - return err - } - - err = downloadedFileput.Close() - if err != nil { - return err - } - case tar.TypeSymlink: - dest := filepath.Join(dst, header.Name) - source := header.Linkname - if err := os.Symlink(source, dest); err != nil { - return err - } - } - } -} - -func download(artifact, dest string) (*os.File, error) { - if err := os.MkdirAll(filepath.Dir(dest), 0700); err != nil { - return nil, err - } - downloadedFile, err := os.Create(dest) - if err != nil { - return nil, err - } - - // Get the data - resp, err := http.Get(artifact) - if err != nil { - return downloadedFile, err - } - - // nolint: errcheck - defer resp.Body.Close() - - if resp.StatusCode != 200 { - // nolint: errcheck - downloadedFile.Close() - return nil, errors.Errorf("failed to download %s, got %d", artifact, resp.StatusCode) - } - - // Write the body to file - _, err = io.Copy(downloadedFile, resp.Body) - if err != nil { - return downloadedFile, err - } - - // Reset downloadedFile file position to 0 so we can immediately read from it - _, err = downloadedFile.Seek(0, 0) - - // TODO add support for checksum validation of downloaded file - - return downloadedFile, err -} diff --git a/internal/pkg/blockdevice/bootloader/bootloader.go b/internal/pkg/installer/bootloader/bootloader.go similarity index 100% rename from internal/pkg/blockdevice/bootloader/bootloader.go rename to internal/pkg/installer/bootloader/bootloader.go diff --git a/internal/pkg/blockdevice/bootloader/syslinux/syslinux.go b/internal/pkg/installer/bootloader/syslinux/syslinux.go similarity index 100% rename from internal/pkg/blockdevice/bootloader/syslinux/syslinux.go rename to internal/pkg/installer/bootloader/syslinux/syslinux.go diff --git a/internal/pkg/blockdevice/bootloader/syslinux/syslinux_test.go b/internal/pkg/installer/bootloader/syslinux/syslinux_test.go similarity index 100% rename from internal/pkg/blockdevice/bootloader/syslinux/syslinux_test.go rename to internal/pkg/installer/bootloader/syslinux/syslinux_test.go diff --git a/internal/pkg/installer/installer.go b/internal/pkg/installer/installer.go new file mode 100644 index 000000000..2938333cc --- /dev/null +++ b/internal/pkg/installer/installer.go @@ -0,0 +1,252 @@ +/* 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 installer + +import ( + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "unsafe" + + "github.com/pkg/errors" + "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" + "github.com/talos-systems/talos/internal/pkg/constants" + "github.com/talos-systems/talos/internal/pkg/installer/bootloader/syslinux" + "github.com/talos-systems/talos/internal/pkg/installer/manifest" + "github.com/talos-systems/talos/internal/pkg/kernel" + "github.com/talos-systems/talos/internal/pkg/mount" + "github.com/talos-systems/talos/internal/pkg/mount/manager" + "github.com/talos-systems/talos/internal/pkg/mount/manager/owned" + "github.com/talos-systems/talos/internal/pkg/version" + "github.com/talos-systems/talos/pkg/userdata" + "golang.org/x/sys/unix" +) + +const ( + // 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 + + // DefaultKernelURL is the URL to the kernel. + DefaultKernelURL = DefaultURLBase + "/vmlinuz" + + // DefaultInitramfsURL is the URL to the initramfs. + DefaultInitramfsURL = DefaultURLBase + "/initramfs.xz" +) + +// Installer represents the installer logic. It serves as the entrypoint to all +// installation methods. +type Installer struct { + cmdline *kernel.Cmdline + data *userdata.UserData + manifest *manifest.Manifest +} + +// NewInstaller initializes and returns an Installer. +func NewInstaller(cmdline *kernel.Cmdline, data *userdata.UserData) *Installer { + i := &Installer{ + cmdline: cmdline, + data: data, + } + + i.manifest = manifest.NewManifest(data) + + return i +} + +// Install fetches the necessary data locations and copies or extracts +// to the target locations. +// nolint: gocyclo +func (i *Installer) Install() (err error) { + if i.data.Install == nil { + return nil + } + + if i.data.Install.Boot != nil { + var ok bool + if ok, err = exists(i.data.Install.Boot.InstallDevice.Device); err != nil { + return err + } + + if ok { + log.Println("found existing installation") + var mountpoints *mount.Points + mountpoints, err = owned.MountPointsFromLabels() + if err != nil { + return err + } + + m := manager.NewManager(mountpoints) + if err = m.MountAll(); err != nil { + return err + } + return nil + } + + if err = VerifyBootDevice(i.data); err != nil { + return errors.Wrap(err, "failed to prepare boot device") + } + } + + // Verify that the target device(s) can satisify the requested options. + + if err = VerifyDataDevice(i.data); err != nil { + return errors.Wrap(err, "failed to prepare data device") + } + + if i.data.Install.Wipe { + if err = wipe(i.manifest); err != nil { + return errors.Wrap(err, "failed to wipe device(s)") + } + } + + // Partition and format the block device(s). + + if err = i.manifest.ExecuteManifest(i.data, i.manifest); err != nil { + return err + } + + // Mount the partitions. + + var mountpoints *mount.Points + if i.data.Install.Boot != nil { + mountpoints, err = owned.MountPointsForDevice(i.data.Install.Boot.InstallDevice.Device) + if err != nil { + return err + } + } else { + mountpoints, err = owned.MountPointsFromLabels() + if err != nil { + return err + } + } + + m := manager.NewManager(mountpoints) + if err = m.MountAll(); err != nil { + return err + } + + // Install the assets. + + for _, targets := range i.manifest.Targets { + for _, target := range targets { + switch target.Label { + case constants.BootPartitionLabel: + // Install the bootloader. + if err = syslinux.Prepare(target.Device); err != nil { + return err + } + case constants.DataPartitionLabel: + continue + } + + // Handle the download and extraction of assets. + if err = target.Save(); err != nil { + return err + } + } + } + + // Install the bootloader. + + if i.data.Install.Boot == nil { + return nil + } + + syslinuxcfg := &syslinux.Cfg{ + Default: "default", + Labels: []*syslinux.Label{ + { + Root: "default", + Kernel: filepath.Join("/", "default", filepath.Base(i.data.Install.Boot.Kernel)), + Initrd: filepath.Join("/", "default", filepath.Base(i.data.Install.Boot.Initramfs)), + Append: i.cmdline.String(), + }, + }, + } + + if err = syslinux.Install(filepath.Join(constants.BootMountPoint), syslinuxcfg); err != nil { + return err + } + + if err = ioutil.WriteFile(filepath.Join(constants.BootMountPoint, "installed"), []byte{}, 0400); err != nil { + return err + } + + return nil +} + +func wipe(manifest *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 +} + +func exists(devpath string) (bool, error) { + var ( + err error + dev *probe.ProbedBlockDevice + ) + + if dev, err = probe.DevForFileSystemLabel(devpath, constants.BootPartitionLabel); err == nil { + // nolint: errcheck + defer dev.Close() + if dev.SuperBlock != nil { + mountpoint := mount.NewMountPoint(dev.Path, "/tmp", dev.SuperBlock.Type(), 0, "") + if err = mountpoint.Mount(); err != nil { + return false, err + } + defer func() { + if err = mountpoint.Unmount(); err != nil { + log.Printf("WARNING: failed to unmount %s from /tmp", dev.Path) + } + }() + _, err = os.Stat(filepath.Join("tmp", "installed")) + switch { + case err == nil: + return true, nil + case os.IsNotExist(err): + return false, nil + default: + return false, err + } + } + } + + return false, nil +} diff --git a/internal/pkg/installer/installer_test.go b/internal/pkg/installer/installer_test.go new file mode 100644 index 000000000..1927511e4 --- /dev/null +++ b/internal/pkg/installer/installer_test.go @@ -0,0 +1,107 @@ +/* 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 installer + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/talos-systems/talos/pkg/userdata" + "gopkg.in/yaml.v2" +) + +type validateSuite struct { + suite.Suite +} + +func TestValidateSuite(t *testing.T) { + suite.Run(t, new(validateSuite)) +} + +func (suite *validateSuite) TestVerifyDevice() { + // Start off with success and then remove bits + data := &userdata.UserData{} + err := yaml.Unmarshal([]byte(testConfig), data) + suite.Require().NoError(err) + + suite.Require().NoError(VerifyBootDevice(data)) + suite.Require().NoError(VerifyDataDevice(data)) + + // No impact because we can infer all data from the data device and + // defaults. + data.Install.Boot = nil + suite.Require().NoError(VerifyBootDevice(data)) + data.Install.Data = &userdata.InstallDevice{ + Device: "/dev/sda", + } + suite.Require().NoError(VerifyDataDevice(data)) +} + +// TODO we should move this to a well defined location +// Copied from userdata_test.go +const testConfig = `version: "1" +security: + os: + ca: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= + identity: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= + kubernetes: + ca: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= + sa: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= + frontproxy: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= + etcd: + crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0= +networking: + os: {} + kubernetes: {} +services: + init: + cni: flannel + kubeadm: + certificateKey: 'test' + configuration: | + apiVersion: kubeadm.k8s.io/v1beta1 + kind: InitConfiguration + localAPIEndpoint: + bindPort: 6443 + bootstrapTokens: + - token: '1qbsj9.3oz5hsk6grdfp98b' + ttl: 0s + --- + apiVersion: kubeadm.k8s.io/v1beta1 + kind: ClusterConfiguration + clusterName: test + kubernetesVersion: v1.14.1 + --- + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + mode: ipvs + ipvs: + scheduler: lc + trustd: + username: 'test' + password: 'test' + endpoints: [ "1.2.3.4" ] + certSANs: [] +install: + wipe: true + force: true + boot: + device: /dev/sda + size: 1024000000 + data: + device: /dev/sda + size: 1024000000 +` diff --git a/internal/pkg/install/prepare.go b/internal/pkg/installer/manifest/manifest.go similarity index 60% rename from internal/pkg/install/prepare.go rename to internal/pkg/installer/manifest/manifest.go index f6ecf6e84..b06a132af 100644 --- a/internal/pkg/install/prepare.go +++ b/internal/pkg/installer/manifest/manifest.go @@ -2,230 +2,55 @@ * 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 +package manifest import ( + "archive/tar" + "compress/gzip" "fmt" "io" "log" + "net/http" "net/url" "os" "path/filepath" "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 - - // 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 - - // 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 = 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 +// Manifest represents the instructions for preparing all block devices +// for an installation. +type Manifest struct { + Targets map[string][]*Target } -// 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 +// 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 } -// 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 == "" { - return errors.New("a data device is required") - } - - 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 data device is defined at this point - // because VerifyDataDevice should have been called first in - // in the chain - data.Install.Boot.Device = data.Install.Data.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 +// Asset represents a file required by a target. +type Asset struct { + Source string + Destination string } // NewManifest initializes and returns a Manifest. @@ -259,7 +84,7 @@ func NewManifest(data *userdata.UserData) (manifest *Manifest) { Destination: filepath.Join("/", "default", filepath.Base(data.Install.Boot.Initramfs)), }, }, - MountPoint: filepath.Join(constants.NewRoot, constants.BootMountPoint), + MountPoint: constants.BootMountPoint, } } @@ -269,7 +94,7 @@ func NewManifest(data *userdata.UserData) (manifest *Manifest) { Size: data.Install.Data.Size, Force: data.Install.Force, Test: false, - MountPoint: filepath.Join(constants.NewRoot, constants.DataMountPoint), + MountPoint: constants.DataMountPoint, } for _, target := range []*Target{bootTarget, dataTarget} { @@ -299,33 +124,37 @@ func NewManifest(data *userdata.UserData) (manifest *Manifest) { return manifest } -// Manifest represents the instructions for preparing all block devices -// for an installation. -type Manifest struct { - Targets map[string][]*Target +// ExecuteManifest partitions and formats all disks in a manifest. +func (m *Manifest) 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 } -// 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 +// 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) @@ -374,7 +203,7 @@ func (t *Target) Partition(bd *blockdevice.BlockDevice) (err error) { return nil } -// Format creates a xfs filesystem on the device/partition +// Format creates a 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 { @@ -387,10 +216,10 @@ func (t *Target) Format() error { return xfs.MakeFS(t.PartitionName, opts...) } -// Install handles downloading the necessary assets and extracting them to -// the appropriate locations +// Save handles downloading the necessary assets and extracting them to +// the appropriate location. // nolint: gocyclo -func (t *Target) Install() error { +func (t *Target) Save() error { // Download and extract all artifacts. var sourceFile *os.File var destFile *os.File @@ -425,7 +254,6 @@ func (t *Target) Install() error { 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 @@ -492,3 +320,120 @@ func (t *Target) Install() error { return nil } + +// nolint: gocyclo, dupl +func untar(tarball *os.File, dst string) error { + + var input io.Reader + var err error + + if strings.HasSuffix(tarball.Name(), ".tar.gz") { + input, err = gzip.NewReader(tarball) + if err != nil { + return err + } + + // nolint: errcheck + defer input.(*gzip.Reader).Close() + } else { + input = tarball + } + + tr := tar.NewReader(input) + + for { + var header *tar.Header + + header, err = tr.Next() + + switch { + case err == io.EOF: + err = nil + return err + case err != nil: + return err + case header == nil: + continue + } + + // the target location where the dir/file should be created + target := filepath.Join(dst, header.Name) + + // May need to add in support for these + /* + // Type '1' to '6' are header-only flags and may not have a data body. + TypeLink = '1' // Hard link + TypeSymlink = '2' // Symbolic link + TypeChar = '3' // Character device node + TypeBlock = '4' // Block device node + TypeDir = '5' // Directory + TypeFifo = '6' // FIFO node + */ + switch header.Typeflag { + case tar.TypeDir: + if err = os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + var downloadedFileput *os.File + + downloadedFileput, err = os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + if _, err = io.Copy(downloadedFileput, tr); err != nil { + return err + } + + err = downloadedFileput.Close() + if err != nil { + return err + } + case tar.TypeSymlink: + dest := filepath.Join(dst, header.Name) + source := header.Linkname + if err := os.Symlink(source, dest); err != nil { + return err + } + } + } +} + +func download(artifact, dest string) (*os.File, error) { + if err := os.MkdirAll(filepath.Dir(dest), 0700); err != nil { + return nil, err + } + downloadedFile, err := os.Create(dest) + if err != nil { + return nil, err + } + + // Get the data + resp, err := http.Get(artifact) + if err != nil { + return downloadedFile, err + } + + // nolint: errcheck + defer resp.Body.Close() + + if resp.StatusCode != 200 { + // nolint: errcheck + downloadedFile.Close() + return nil, errors.Errorf("failed to download %s, got %d", artifact, resp.StatusCode) + } + + // Write the body to file + _, err = io.Copy(downloadedFile, resp.Body) + if err != nil { + return downloadedFile, err + } + + // Reset downloadedFile file position to 0 so we can immediately read from it + _, err = downloadedFile.Seek(0, 0) + + // TODO add support for checksum validation of downloaded file + + return downloadedFile, err +} diff --git a/internal/pkg/install/prepare_test.go b/internal/pkg/installer/manifest/manifest_test.go similarity index 82% rename from internal/pkg/install/prepare_test.go rename to internal/pkg/installer/manifest/manifest_test.go index 0bc2c350c..f5e6e8c93 100644 --- a/internal/pkg/install/prepare_test.go +++ b/internal/pkg/installer/manifest/manifest_test.go @@ -2,7 +2,7 @@ * 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 +package manifest import ( "io/ioutil" @@ -18,15 +18,15 @@ import ( "gopkg.in/yaml.v2" ) -type validateSuite struct { +type manifestSuite struct { suite.Suite } -func TestValidateSuite(t *testing.T) { - suite.Run(t, new(validateSuite)) +func TestManifestSuite(t *testing.T) { + suite.Run(t, new(manifestSuite)) } -func (suite *validateSuite) TestNewManifest() { +func (suite *manifestSuite) TestNewManifest() { // Test with whole data data := &userdata.UserData{} err := yaml.Unmarshal([]byte(testConfig), data) @@ -36,26 +36,7 @@ func (suite *validateSuite) TestNewManifest() { assert.Equal(suite.T(), 2, len(manifests.Targets["/dev/sda"])) } -func (suite *validateSuite) TestVerifyDevice() { - // Start off with success and then remove bits - data := &userdata.UserData{} - err := yaml.Unmarshal([]byte(testConfig), data) - suite.Require().NoError(err) - - suite.Require().NoError(VerifyBootDevice(data)) - suite.Require().NoError(VerifyDataDevice(data)) - - // No impact because we can infer all data from the data device and - // defaults. - data.Install.Boot = nil - suite.Require().NoError(VerifyBootDevice(data)) - data.Install.Data = &userdata.InstallDevice{ - Device: "/dev/sda", - } - suite.Require().NoError(VerifyDataDevice(data)) -} - -func (suite *validateSuite) TestTargetInstall() { +func (suite *manifestSuite) TestTargetInstall() { // Create Temp dirname for mountpoint dir, err := ioutil.TempDir("", "talostest") suite.Require().NoError(err) @@ -95,7 +76,7 @@ func (suite *validateSuite) TestTargetInstall() { }, } - suite.Require().NoError(target.Install()) + suite.Require().NoError(target.Save()) for _, expectedFile := range target.Assets { // Verify downloaded/copied file is at the appropriate location diff --git a/internal/pkg/installer/verify.go b/internal/pkg/installer/verify.go new file mode 100644 index 000000000..b05e1fe21 --- /dev/null +++ b/internal/pkg/installer/verify.go @@ -0,0 +1,83 @@ +/* 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 installer + +import ( + "github.com/pkg/errors" + "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" + "github.com/talos-systems/talos/internal/pkg/constants" + "github.com/talos-systems/talos/pkg/userdata" +) + +// 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 == "" { + return errors.New("a data device is required") + } + + 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 data device is defined at this point + // because VerifyDataDevice should have been called first in + // in the chain + data.Install.Boot.Device = data.Install.Data.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 +} diff --git a/internal/pkg/kernel/kernel.go b/internal/pkg/kernel/kernel.go index 06dad32b3..819943ba8 100644 --- a/internal/pkg/kernel/kernel.go +++ b/internal/pkg/kernel/kernel.go @@ -13,7 +13,7 @@ import ( // NewDefaultCmdline returns a set of kernel parameters that serve as the base // for all Talos installations. // nolint: golint -func NewDefaultCmdline() *cmdline { +func NewDefaultCmdline() *Cmdline { cmdline := NewCmdline("") cmdline.Append("page_poison", "1") cmdline.Append("slab_nomerge", "") @@ -122,17 +122,18 @@ func (p Parameters) String() string { return strings.TrimRight(s, " ") } -type cmdline struct { +// Cmdline represents a set of kernel parameters. +type Cmdline struct { sync.Mutex Parameters } -var instance *cmdline +var instance *Cmdline var once sync.Once // Cmdline returns a representation of /proc/cmdline. // nolint: golint -func Cmdline() *cmdline { +func ProcCmdline() *Cmdline { once.Do(func() { var err error if instance, err = read(); err != nil { @@ -146,15 +147,15 @@ func Cmdline() *cmdline { // NewCmdline initializes and returns a representation of the cmdline values // specified by `parameters`. // nolint: golint -func NewCmdline(parameters string) *cmdline { +func NewCmdline(parameters string) *Cmdline { parsed := parse(parameters) - c := &cmdline{sync.Mutex{}, parsed} + c := &Cmdline{sync.Mutex{}, parsed} return c } // Get gets a kernel parameter. -func (c *cmdline) Get(k string) (value *Parameter) { +func (c *Cmdline) Get(k string) (value *Parameter) { c.Lock() defer c.Unlock() for _, value := range c.Parameters { @@ -167,7 +168,7 @@ func (c *cmdline) Get(k string) (value *Parameter) { } // Set sets a kernel parameter. -func (c *cmdline) Set(k string, v *Parameter) { +func (c *Cmdline) Set(k string, v *Parameter) { c.Lock() defer c.Unlock() for i, value := range c.Parameters { @@ -179,7 +180,7 @@ func (c *cmdline) Set(k string, v *Parameter) { } // Append appends a kernel parameter. -func (c *cmdline) Append(k string, v string) { +func (c *Cmdline) Append(k string, v string) { c.Lock() defer c.Unlock() for _, value := range c.Parameters { @@ -192,7 +193,7 @@ func (c *cmdline) Append(k string, v string) { } // AppendAll appends a set of kernel parameters. -func (c *cmdline) AppendAll(args []string) error { +func (c *Cmdline) AppendAll(args []string) error { parameters := parse(strings.Join(args, " ")) c.Parameters = append(c.Parameters, parameters...) @@ -200,7 +201,7 @@ func (c *cmdline) AppendAll(args []string) error { } // Bytes returns the byte slice representation of the cmdline struct. -func (c *cmdline) Bytes() []byte { +func (c *Cmdline) Bytes() []byte { return []byte(c.String()) } @@ -231,7 +232,7 @@ func parse(parameters string) (parsed Parameters) { return parsed } -func read() (c *cmdline, err error) { +func read() (c *Cmdline, err error) { var parameters []byte parameters, err = ioutil.ReadFile("/proc/cmdline") if err != nil { @@ -239,7 +240,7 @@ func read() (c *cmdline, err error) { } parsed := parse(string(parameters)) - c = &cmdline{sync.Mutex{}, parsed} + c = &Cmdline{sync.Mutex{}, parsed} return c, nil } diff --git a/internal/pkg/kernel/sysctl/sysctl.go b/internal/pkg/kernel/sysctl/sysctl.go new file mode 100644 index 000000000..f7bd06f2d --- /dev/null +++ b/internal/pkg/kernel/sysctl/sysctl.go @@ -0,0 +1,32 @@ +/* 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 sysctl + +import ( + "io/ioutil" + "path" + "strings" +) + +// SystemProperty represents a kernel system property. +type SystemProperty struct { + Key string + Value string +} + +// WriteSystemProperty writes a value to a key under /proc/sys. +func WriteSystemProperty(prop *SystemProperty) error { + return ioutil.WriteFile(prop.Path(), []byte(prop.Value), 0644) +} + +// ReadSystemProperty reads a value from a key under /proc/sys. +func ReadSystemProperty(prop *SystemProperty) ([]byte, error) { + return ioutil.ReadFile(prop.Path()) +} + +// Path returns the path to the systctl file under /proc/sys. +func (prop *SystemProperty) Path() string { + return path.Join("/proc/sys", strings.Replace(prop.Key, ".", "/", -1)) +} diff --git a/internal/pkg/mount/manager/cgroups/cgroups.go b/internal/pkg/mount/manager/cgroups/cgroups.go new file mode 100644 index 000000000..b4c1228ee --- /dev/null +++ b/internal/pkg/mount/manager/cgroups/cgroups.go @@ -0,0 +1,41 @@ +/* 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 cgroups + +import ( + "path" + + "github.com/talos-systems/talos/internal/pkg/mount" + + "golang.org/x/sys/unix" +) + +// MountPoints returns the cgroup mount points +func MountPoints() (mountpoints *mount.Points, err error) { + base := "/sys/fs/cgroup" + cgroups := mount.NewMountPoints() + cgroups.Set("dev", mount.NewMountPoint("tmpfs", base, "tmpfs", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "mode=755")) + + controllers := []string{ + "blkio", + "cpu", + "cpuacct", + "cpuset", + "devices", + "freezer", + "hugetlb", + "memory", + "net_cls", + "net_prio", + "perf_event", + "pids", + } + for _, c := range controllers { + p := path.Join(base, c) + cgroups.Set(c, mount.NewMountPoint(c, p, "cgroup", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, c)) + } + + return cgroups, nil +} diff --git a/internal/app/machined/internal/mount/cgroups/cgroups_test.go b/internal/pkg/mount/manager/cgroups/cgroups_test.go similarity index 100% rename from internal/app/machined/internal/mount/cgroups/cgroups_test.go rename to internal/pkg/mount/manager/cgroups/cgroups_test.go diff --git a/internal/pkg/mount/manager/manager.go b/internal/pkg/mount/manager/manager.go new file mode 100644 index 000000000..1ac3f2af1 --- /dev/null +++ b/internal/pkg/mount/manager/manager.go @@ -0,0 +1,92 @@ +/* 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 manager + +import ( + "github.com/pkg/errors" + "github.com/talos-systems/talos/internal/pkg/mount" +) + +// Manager represents a management layer for a set of mountpoints. +type Manager struct { + mountpoints *mount.Points +} + +// NewManager initializes and returns a Manager. +func NewManager(mountpoints *mount.Points) *Manager { + m := &Manager{ + mountpoints: mountpoints, + } + + return m +} + +// MountAll mounts the device(s). +func (m *Manager) MountAll() (err error) { + iter := m.mountpoints.Iter() + + // Mount the device(s). + + for iter.Next() { + mountpoint := iter.Value() + // Repair the disk's partition table. + if mountpoint.Resize { + if err = mountpoint.ResizePartition(); err != nil { + return errors.Wrap(err, "resize") + } + } + + if err = mountpoint.Mount(); err != nil { + return errors.Wrap(err, "mount") + } + + // Grow the filesystem to the maximum allowed size. + if mountpoint.Resize { + if err = mountpoint.GrowFilesystem(); err != nil { + return errors.Wrap(err, "grow") + } + } + } + if iter.Err() != nil { + return iter.Err() + } + + return nil +} + +// UnmountAll unmounts the device(s). +func (m *Manager) UnmountAll() (err error) { + iter := m.mountpoints.IterRev() + for iter.Next() { + mountpoint := iter.Value() + if err = mountpoint.Unmount(); err != nil { + return errors.Wrap(err, "unomunt") + } + } + if iter.Err() != nil { + return iter.Err() + } + + return nil +} + +// MoveAll moves the device(s). +// TODO(andrewrynhard): We need to skip calling the move method on mountpoints +// that are a child of another mountpoint. The kernel will handle moving the +// child mountpoints for us. +func (m *Manager) MoveAll(prefix string) (err error) { + iter := m.mountpoints.Iter() + for iter.Next() { + mountpoint := iter.Value() + if err = mountpoint.Move(prefix); err != nil { + return errors.Wrapf(err, "move") + } + } + if iter.Err() != nil { + return iter.Err() + } + + return nil +} diff --git a/internal/app/init/internal/mount/mount_test.go b/internal/pkg/mount/manager/manager_test.go similarity index 94% rename from internal/app/init/internal/mount/mount_test.go rename to internal/pkg/mount/manager/manager_test.go index bf5c87617..a6cca4edd 100644 --- a/internal/app/init/internal/mount/mount_test.go +++ b/internal/pkg/mount/manager/manager_test.go @@ -2,7 +2,7 @@ * 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 mount_test +package manager_test import "testing" diff --git a/internal/pkg/install/mount.go b/internal/pkg/mount/manager/owned/owned.go similarity index 50% rename from internal/pkg/install/mount.go rename to internal/pkg/mount/manager/owned/owned.go index 4dc4a4ff3..91c9be4eb 100644 --- a/internal/pkg/install/mount.go +++ b/internal/pkg/mount/manager/owned/owned.go @@ -2,7 +2,7 @@ * 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 +package owned import ( "log" @@ -11,34 +11,11 @@ import ( "github.com/talos-systems/talos/internal/pkg/blockdevice/probe" "github.com/talos-systems/talos/internal/pkg/constants" "github.com/talos-systems/talos/internal/pkg/mount" - "github.com/talos-systems/talos/pkg/userdata" "golang.org/x/sys/unix" ) -// Mount discovers the appropriate partitions by label and mounts them up -// to the appropriate mountpoint. -// TODO: See if we can consolidate this with rootfs/mount -func Mount(data *userdata.UserData) (err error) { - var mp *mount.Points - if mp, err = mountpoints(data.Install.Boot.InstallDevice.Device); err != nil { - return errors.Errorf("error initializing block devices: %v", err) - } - - iter := mp.Iter() - for iter.Next() { - if err = mount.WithRetry(iter.Value(), mount.WithPrefix(constants.NewRoot)); err != nil { - return errors.Errorf("error mounting partitions: %v", err) - } - } - if iter.Err() != nil { - return iter.Err() - } - - return nil -} - -// nolint: dupl -func mountpoints(devpath string) (mountpoints *mount.Points, err error) { +// MountPointsForDevice returns the mountpoints required to boot the system. +func MountPointsForDevice(devpath string) (mountpoints *mount.Points, err error) { mountpoints = mount.NewMountPoints() for _, name := range []string{constants.DataPartitionLabel, constants.BootPartitionLabel} { var target string @@ -56,13 +33,38 @@ func mountpoints(devpath string) (mountpoints *mount.Points, err error) { log.Println("WARNING: no ESP partition was found") continue } - return nil, errors.Errorf("failed to find device with label %s: %v", name, err) + return nil, errors.Errorf("probe device for filesystem %s: %v", name, err) } - mountpoint := mount.NewMountPoint(dev.Path, target, dev.SuperBlock.Type(), unix.MS_NOATIME, "") - mountpoints.Set(name, mountpoint) } return mountpoints, nil } + +// MountPointsFromLabels returns the mountpoints required to boot the system. +func MountPointsFromLabels() (mountpoints *mount.Points, err error) { + mountpoints = mount.NewMountPoints() + for _, name := range []string{constants.DataPartitionLabel, constants.BootPartitionLabel} { + var target string + switch name { + case constants.DataPartitionLabel: + target = constants.DataMountPoint + case constants.BootPartitionLabel: + target = constants.BootMountPoint + } + + var dev *probe.ProbedBlockDevice + if dev, err = probe.GetDevWithFileSystemLabel(name); err != nil { + if name == constants.BootPartitionLabel { + // A bootloader is not always required. + log.Println("WARNING: no ESP partition was found") + continue + } + return nil, errors.Errorf("find device with label %s: %v", name, err) + } + mountpoint := mount.NewMountPoint(dev.Path, target, dev.SuperBlock.Type(), unix.MS_NOATIME, "") + mountpoints.Set(name, mountpoint) + } + return mountpoints, nil +} diff --git a/internal/app/machined/internal/mount/mount_test.go b/internal/pkg/mount/manager/owned/owned_test.go similarity index 95% rename from internal/app/machined/internal/mount/mount_test.go rename to internal/pkg/mount/manager/owned/owned_test.go index bf5c87617..16ce9d52d 100644 --- a/internal/app/machined/internal/mount/mount_test.go +++ b/internal/pkg/mount/manager/owned/owned_test.go @@ -2,7 +2,7 @@ * 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 mount_test +package owned_test import "testing" diff --git a/internal/pkg/mount/manager/squashfs/squashfs.go b/internal/pkg/mount/manager/squashfs/squashfs.go new file mode 100644 index 000000000..d03e04805 --- /dev/null +++ b/internal/pkg/mount/manager/squashfs/squashfs.go @@ -0,0 +1,25 @@ +/* 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 squashfs + +import ( + "github.com/talos-systems/talos/internal/pkg/constants" + "github.com/talos-systems/talos/internal/pkg/mount" + "golang.org/x/sys/unix" + "gopkg.in/freddierice/go-losetup.v1" +) + +// MountPoints returns the mountpoints required to boot the system. +func MountPoints(prefix string) (mountpoints *mount.Points, err error) { + var dev losetup.Device + dev, err = losetup.Attach("/"+constants.RootfsAsset, 0, true) + if err != nil { + return nil, err + } + squashfs := mount.NewMountPoints() + squashfs.Set("squashfs", mount.NewMountPoint(dev.Path(), "/", "squashfs", unix.MS_RDONLY, "", mount.WithPrefix(prefix), mount.WithReadOnly(true), mount.WithShared(true))) + + return squashfs, nil +} diff --git a/internal/pkg/mount/manager/squashfs/squashfs_test.go b/internal/pkg/mount/manager/squashfs/squashfs_test.go new file mode 100644 index 000000000..e5aedff2c --- /dev/null +++ b/internal/pkg/mount/manager/squashfs/squashfs_test.go @@ -0,0 +1,14 @@ +/* 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 squashfs_test + +import "testing" + +func TestEmpty(t *testing.T) { + // added for accurate coverage estimation + // + // please remove it once any unit-test is added + // for this package +} diff --git a/internal/pkg/mount/manager/virtual/virtual.go b/internal/pkg/mount/manager/virtual/virtual.go new file mode 100644 index 000000000..9a6ea7dea --- /dev/null +++ b/internal/pkg/mount/manager/virtual/virtual.go @@ -0,0 +1,31 @@ +/* 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 virtual + +import ( + "github.com/talos-systems/talos/internal/pkg/mount" + "golang.org/x/sys/unix" +) + +// MountPoints returns the mountpoints required to boot the system. +func MountPoints() (mountpoints *mount.Points, err error) { + virtual := mount.NewMountPoints() + virtual.Set("dev", mount.NewMountPoint("devtmpfs", "/dev", "devtmpfs", unix.MS_NOSUID, "mode=0755")) + virtual.Set("proc", mount.NewMountPoint("proc", "/proc", "proc", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")) + virtual.Set("sys", mount.NewMountPoint("sysfs", "/sys", "sysfs", 0, "")) + virtual.Set("run", mount.NewMountPoint("tmpfs", "/run", "tmpfs", 0, "")) + virtual.Set("tmp", mount.NewMountPoint("tmpfs", "/tmp", "tmpfs", 0, "")) + + return virtual, nil +} + +// SubMountPoints returns the mountpoints required to boot the system. +func SubMountPoints() (mountpoints *mount.Points, err error) { + virtual := mount.NewMountPoints() + virtual.Set("devshm", mount.NewMountPoint("tmpfs", "/dev/shm", "tmpfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, "")) + virtual.Set("devpts", mount.NewMountPoint("devpts", "/dev/pts", "devpts", unix.MS_NOSUID|unix.MS_NOEXEC, "ptmxmode=000,mode=620,gid=5")) + + return virtual, nil +} diff --git a/internal/app/machined/internal/phase/install/install_test.go b/internal/pkg/mount/manager/virtual/virtual_test.go similarity index 94% rename from internal/app/machined/internal/phase/install/install_test.go rename to internal/pkg/mount/manager/virtual/virtual_test.go index eee2c96cc..60e28f767 100644 --- a/internal/app/machined/internal/phase/install/install_test.go +++ b/internal/pkg/mount/manager/virtual/virtual_test.go @@ -2,7 +2,7 @@ * 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_test +package virtual_test import "testing" diff --git a/internal/pkg/mount/mount.go b/internal/pkg/mount/mount.go index 201b6e11f..b27ea28c0 100644 --- a/internal/pkg/mount/mount.go +++ b/internal/pkg/mount/mount.go @@ -10,6 +10,11 @@ import ( "time" "github.com/pkg/errors" + "github.com/talos-systems/talos/internal/pkg/blockdevice" + "github.com/talos-systems/talos/internal/pkg/blockdevice/filesystem/xfs" + gptpartition "github.com/talos-systems/talos/internal/pkg/blockdevice/table/gpt/partition" + "github.com/talos-systems/talos/internal/pkg/blockdevice/util" + "github.com/talos-systems/talos/internal/pkg/constants" "golang.org/x/sys/unix" ) @@ -20,6 +25,7 @@ type Point struct { fstype string flags uintptr data string + *Options } // PointMap represents a unique set of mount points. @@ -32,13 +38,15 @@ type Points struct { } // NewMountPoint initializes and returns a Point struct. -func NewMountPoint(source string, target string, fstype string, flags uintptr, data string) *Point { +func NewMountPoint(source string, target string, fstype string, flags uintptr, data string, setters ...Option) *Point { + opts := NewDefaultOptions(setters...) return &Point{ - source: source, - target: target, - fstype: fstype, - flags: flags, - data: data, + source: source, + target: target, + fstype: fstype, + flags: flags, + data: data, + Options: opts, } } @@ -74,22 +82,20 @@ func (p *Point) Data() string { return p.data } -// WithRetry attempts to retry a mount on EBUSY. It will attempt a retry +// Mount attempts to retry a mount on EBUSY. It will attempt a retry // every 100 milliseconds over the course of 5 seconds. -func WithRetry(mountpoint *Point, setters ...Option) (err error) { - opts := NewDefaultOptions(setters...) - - if opts.ReadOnly { - mountpoint.flags |= unix.MS_RDONLY +func (p *Point) Mount() (err error) { + if p.ReadOnly { + p.flags |= unix.MS_RDONLY } - target := path.Join(opts.Prefix, mountpoint.target) + target := path.Join(p.Prefix, p.target) if err = os.MkdirAll(target, os.ModeDir); err != nil { return errors.Errorf("error creating mount point directory %s: %v", target, err) } - retry := func(source string, target string, fstype string, flags uintptr, data string) error { + retry := func(source string, target string, fstype string, flags uintptr, data string) (err error) { for i := 0; i < 50; i++ { if err = unix.Mount(source, target, fstype, flags, data); err != nil { switch err { @@ -103,14 +109,15 @@ func WithRetry(mountpoint *Point, setters ...Option) (err error) { return nil } + return errors.Errorf("mount timeout: %v", err) } - if err = retry(mountpoint.source, target, mountpoint.fstype, mountpoint.flags, mountpoint.data); err != nil { + if err = retry(p.source, target, p.fstype, p.flags, p.data); err != nil { return err } - if opts.Shared { + if p.Shared { if err = retry("", target, "", unix.MS_SHARED, ""); err != nil { return errors.Errorf("error mounting shared mount point %s: %v", target, err) } @@ -120,11 +127,9 @@ func WithRetry(mountpoint *Point, setters ...Option) (err error) { } -// UnWithRetry attempts to retry an unmount on EBUSY. It will attempt a +// Unmount attempts to retry an unmount on EBUSY. It will attempt a // retry every 100 milliseconds over the course of 5 seconds. -func UnWithRetry(mountpoint *Point, setters ...Option) (err error) { - opts := NewDefaultOptions(setters...) - +func (p *Point) Unmount() (err error) { retry := func(target string, flags int) error { for i := 0; i < 50; i++ { if err = unix.Unmount(target, flags); err != nil { @@ -141,10 +146,74 @@ func UnWithRetry(mountpoint *Point, setters ...Option) (err error) { return errors.Errorf("mount timeout: %v", err) } - target := path.Join(opts.Prefix, mountpoint.target) + target := path.Join(p.Prefix, p.target) if err := retry(target, 0); err != nil { return err } return nil } + +// Move moves a mountpoint to a new location with a prefix. +func (p *Point) Move(prefix string) (err error) { + target := p.Target() + mountpoint := NewMountPoint(target, target, "", unix.MS_MOVE, "", WithPrefix(prefix)) + if err = mountpoint.Mount(); err != nil { + return errors.Errorf("error moving mount point %s: %v", target, err) + } + + return nil +} + +// ResizePartition resizes a partition to the maximum size allowed. +func (p *Point) ResizePartition() (err error) { + var devname string + if devname, err = util.DevnameFromPartname(p.Source()); err != nil { + return err + } + bd, err := blockdevice.Open("/dev/" + devname) + if err != nil { + return errors.Errorf("error opening block device %q: %v", devname, err) + } + // nolint: errcheck + defer bd.Close() + + pt, err := bd.PartitionTable(true) + if err != nil { + return err + } + + if err := pt.Repair(); err != nil { + return err + } + + for _, partition := range pt.Partitions() { + if partition.(*gptpartition.Partition).Name == constants.DataPartitionLabel { + if err := pt.Resize(partition); err != nil { + return err + } + } + } + + if err := pt.Write(); err != nil { + return err + } + + // NB: Rereading the partition table requires that all partitions be + // unmounted or it will fail with EBUSY. + if err := bd.RereadPartitionTable(); err != nil { + return err + } + + return nil +} + +// GrowFilesystem grows a partition's filesystem to the maximum size allowed. +// NB: An XFS partition MUST be mounted, or this will fail. +func (p *Point) GrowFilesystem() (err error) { + if err = xfs.GrowFS(p.Target()); err != nil { + return errors.Wrap(err, "xfs_growfs") + } + + return nil +} diff --git a/internal/pkg/mount/options.go b/internal/pkg/mount/options.go index e7ac7b85a..3d46c083b 100644 --- a/internal/pkg/mount/options.go +++ b/internal/pkg/mount/options.go @@ -6,9 +6,11 @@ package mount // Options is the functional options struct. type Options struct { + Loopback string Prefix string ReadOnly bool Shared bool + Resize bool } // Option is the functional option func. @@ -35,9 +37,18 @@ func WithShared(o bool) Option { } } +// WithResize indicates that a the partition for a given mount point should be +// resized to the maximum allowed. +func WithResize(o bool) Option { + return func(args *Options) { + args.Resize = o + } +} + // NewDefaultOptions initializes a Options struct with default values. func NewDefaultOptions(setters ...Option) *Options { opts := &Options{ + Loopback: "", Prefix: "", ReadOnly: false, Shared: false, diff --git a/internal/pkg/mount/switchroot/switchroot.go b/internal/pkg/mount/switchroot/switchroot.go new file mode 100644 index 000000000..d662ec9a8 --- /dev/null +++ b/internal/pkg/mount/switchroot/switchroot.go @@ -0,0 +1,118 @@ +/* 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 switchroot + +import ( + "log" + "os" + + "github.com/pkg/errors" + "github.com/talos-systems/talos/internal/pkg/mount/manager" + "golang.org/x/sys/unix" +) + +// Switch moves the rootfs to a specified directory. See +// https://github.com/karelzak/util-linux/blob/master/sys-utils/switch_root.c. +// nolint: gocyclo +func Switch(prefix string, virtual *manager.Manager) (err error) { + log.Println("moving virtual devices to the new rootfs") + if err = virtual.MoveAll(prefix); err != nil { + return err + } + + log.Printf("changing working directory into %s", prefix) + if err = unix.Chdir(prefix); err != nil { + return errors.Wrapf(err, "error changing working directory to %s", prefix) + } + + var old *os.File + if old, err = os.Open("/"); err != nil { + return errors.Wrap(err, "error opening /") + } + // nolint: errcheck + defer old.Close() + + log.Printf("moving %s to /", prefix) + if err = unix.Mount(prefix, "/", "", unix.MS_MOVE, ""); err != nil { + return errors.Wrap(err, "error moving /") + } + + log.Println("changing root directory") + if err = unix.Chroot("."); err != nil { + return errors.Wrap(err, "error chroot") + } + + log.Println("cleaning up initramfs") + if err = recursiveDelete(int(old.Fd())); err != nil { + return errors.Wrap(err, "error deleting initramfs") + } + + // Note that /sbin/init is machined. We call it init since this is the + // convention. + log.Println("executing /sbin/init") + if err = unix.Exec("/sbin/init", []string{"/sbin/init"}, []string{}); err != nil { + return errors.Wrap(err, "error executing /sbin/init") + } + + return nil +} + +func recursiveDelete(fd int) error { + parentDev, err := getDev(fd) + if err != nil { + return err + } + + dir := os.NewFile(uintptr(fd), "__ignored__") + // nolint: errcheck + defer dir.Close() + names, err := dir.Readdirnames(-1) + if err != nil { + return err + } + + for _, name := range names { + if err := recusiveDeleteInner(fd, parentDev, name); err != nil { + return err + } + } + return nil +} + +func recusiveDeleteInner(parentFd int, parentDev uint64, childName string) error { + childFd, err := unix.Openat(parentFd, childName, unix.O_DIRECTORY|unix.O_NOFOLLOW, unix.O_RDWR) + if err != nil { + if err := unix.Unlinkat(parentFd, childName, 0); err != nil { + return err + } + } else { + // nolint: errcheck + defer unix.Close(childFd) + + if childFdDev, err := getDev(childFd); err != nil { + return err + } else if childFdDev != parentDev { + return nil + } + + if err := recursiveDelete(childFd); err != nil { + return err + } + if err := unix.Unlinkat(parentFd, childName, unix.AT_REMOVEDIR); err != nil { + return err + } + } + return nil +} + +func getDev(fd int) (dev uint64, err error) { + var stat unix.Stat_t + + if err := unix.Fstat(fd, &stat); err != nil { + return 0, err + } + + return stat.Dev, nil +} diff --git a/internal/pkg/mount/switchroot/switchroot_test.go b/internal/pkg/mount/switchroot/switchroot_test.go new file mode 100644 index 000000000..33688a98c --- /dev/null +++ b/internal/pkg/mount/switchroot/switchroot_test.go @@ -0,0 +1,14 @@ +/* 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 switchroot_test + +import "testing" + +func TestEmpty(t *testing.T) { + // added for accurate coverage estimation + // + // please remove it once any unit-test is added + // for this package +} diff --git a/internal/pkg/network/dhcp.go b/internal/pkg/network/dhcp.go index 080a21e29..00a3e0a6e 100644 --- a/internal/pkg/network/dhcp.go +++ b/internal/pkg/network/dhcp.go @@ -116,7 +116,7 @@ func (service *Service) dhclient4(ctx context.Context, ifname string, modifiers // Ignore DHCP-offered hostname if the kernel parameter is set var kernHostname *string - if kernHostname = kernel.Cmdline().Get(constants.KernelParamHostname).First(); kernHostname != nil { + if kernHostname = kernel.ProcCmdline().Get(constants.KernelParamHostname).First(); kernHostname != nil { hostname = *kernHostname } diff --git a/internal/pkg/network/setup.go b/internal/pkg/network/setup.go index 2fc927457..9d0057a09 100644 --- a/internal/pkg/network/setup.go +++ b/internal/pkg/network/setup.go @@ -123,7 +123,7 @@ func ifup(ifname string, mtu int) (err error) { func defaultInterface() string { netif := DefaultInterface - if option := kernel.Cmdline().Get(constants.KernelParamDefaultInterface).First(); option != nil { + if option := kernel.ProcCmdline().Get(constants.KernelParamDefaultInterface).First(); option != nil { netif = *option } diff --git a/internal/pkg/proc/proc.go b/internal/pkg/proc/proc.go index 85fc9ff70..d86dceb57 100644 --- a/internal/pkg/proc/proc.go +++ b/internal/pkg/proc/proc.go @@ -6,26 +6,12 @@ package proc import ( "fmt" - "io/ioutil" - "path" "strings" "code.cloudfoundry.org/bytefmt" "github.com/prometheus/procfs" ) -// SystemProperty represents a kernel system property. -type SystemProperty struct { - Key string - Value string -} - -// WriteSystemProperty writes a value to a key under /proc/sys. -func WriteSystemProperty(prop *SystemProperty) error { - keyPath := path.Join("/proc/sys", strings.Replace(prop.Key, ".", "/", -1)) - return ioutil.WriteFile(keyPath, []byte(prop.Value), 0644) -} - // ProcessList contains all of the process stats we want // to display via top type ProcessList struct { diff --git a/internal/pkg/security/kspp/kspp.go b/internal/pkg/security/kspp/kspp.go index 0632e4753..d5da0cf81 100644 --- a/internal/pkg/security/kspp/kspp.go +++ b/internal/pkg/security/kspp/kspp.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "github.com/talos-systems/talos/internal/pkg/kernel" - "github.com/talos-systems/talos/internal/pkg/proc" + "github.com/talos-systems/talos/internal/pkg/kernel/sysctl" ) var ( @@ -28,7 +28,7 @@ func EnforceKSPPKernelParameters() error { var result *multierror.Error for _, values := range RequiredKSPPKernelParameters { var val *string - if val = kernel.Cmdline().Get(values.Key()).First(); val == nil { + if val = kernel.ProcCmdline().Get(values.Key()).First(); val == nil { result = multierror.Append(result, errors.Errorf("KSPP kernel parameter %s is required", values.Key())) continue } @@ -45,7 +45,7 @@ func EnforceKSPPKernelParameters() error { // EnforceKSPPSysctls verifies that all required KSPP kernel sysctls are set // with the right value. func EnforceKSPPSysctls() (err error) { - props := []*proc.SystemProperty{ + props := []*sysctl.SystemProperty{ { Key: "kernel.kptr_restrict", Value: "1", @@ -82,7 +82,7 @@ func EnforceKSPPSysctls() (err error) { } for _, prop := range props { - if err = proc.WriteSystemProperty(prop); err != nil { + if err = sysctl.WriteSystemProperty(prop); err != nil { return } } diff --git a/internal/pkg/upgrade/upgrade.go b/internal/pkg/upgrade/upgrade.go index 173ea61b2..68a12a7f1 100644 --- a/internal/pkg/upgrade/upgrade.go +++ b/internal/pkg/upgrade/upgrade.go @@ -14,9 +14,9 @@ import ( "time" "github.com/pkg/errors" - "github.com/talos-systems/talos/internal/pkg/blockdevice/bootloader/syslinux" "github.com/talos-systems/talos/internal/pkg/constants" - "github.com/talos-systems/talos/internal/pkg/install" + "github.com/talos-systems/talos/internal/pkg/installer/bootloader/syslinux" + "github.com/talos-systems/talos/internal/pkg/installer/manifest" "github.com/talos-systems/talos/internal/pkg/kernel" "github.com/talos-systems/talos/internal/pkg/kubernetes" "github.com/talos-systems/talos/pkg/userdata" @@ -83,31 +83,31 @@ func NewUpgrade(url string) (err error) { } func upgradeBoot(url string) error { - bootTarget := install.Target{ + bootTarget := manifest.Target{ Label: constants.BootPartitionLabel, MountPoint: constants.BootMountPoint, - Assets: []*install.Asset{}, + Assets: []*manifest.Asset{}, } // Kernel - bootTarget.Assets = append(bootTarget.Assets, &install.Asset{ + bootTarget.Assets = append(bootTarget.Assets, &manifest.Asset{ Source: url + "/" + constants.KernelAsset, Destination: filepath.Join("/", "default", constants.KernelAsset), }) // Initramfs - bootTarget.Assets = append(bootTarget.Assets, &install.Asset{ + bootTarget.Assets = append(bootTarget.Assets, &manifest.Asset{ Source: url + "/" + constants.InitramfsAsset, Destination: filepath.Join("/", "default", constants.InitramfsAsset), }) var err error - if err = bootTarget.Install(); err != nil { + if err = bootTarget.Save(); err != nil { return err } // TODO: Figure out a method to update kernel args - nextCmdline := kernel.NewCmdline(kernel.Cmdline().String()) + nextCmdline := kernel.NewCmdline(kernel.ProcCmdline().String()) // Set the initrd kernel paramaeter. initParam := kernel.NewParameter("initrd")