talos/internal/pkg/mount/mount.go
Artem Chernyshev 2dc27d9964 fix: do not format state partition in the initialize sequence
Initialize state should be only reading the config.
So now if it detects that the partition is not even formatted it will
skip it and will consider the state to be empty.

Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
2021-06-17 07:17:42 -07:00

432 lines
9.8 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package mount
import (
"bufio"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/talos-systems/go-blockdevice/blockdevice"
"github.com/talos-systems/go-blockdevice/blockdevice/filesystem"
"github.com/talos-systems/go-blockdevice/blockdevice/util"
"github.com/talos-systems/go-retry/retry"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/makefs"
)
// RetryFunc defines the requirements for retrying a mount point operation.
type RetryFunc func(*Point) error
// Mount mounts the device(s).
func Mount(mountpoints *Points) (err error) {
iter := mountpoints.Iter()
// Mount the device(s).
for iter.Next() {
if err = mountMountpoint(iter.Value()); err != nil {
return fmt.Errorf("error mounting %q: %w", iter.Value().Source(), err)
}
}
if iter.Err() != nil {
return iter.Err()
}
return nil
}
//nolint:gocyclo
func mountMountpoint(mountpoint *Point) (err error) {
var skipMount bool
// Repair the disk's partition table.
if mountpoint.MountFlags.Check(Resize) {
if _, err = mountpoint.ResizePartition(); err != nil {
return fmt.Errorf("error resizing %w", err)
}
}
if mountpoint.MountFlags.Check(SkipIfMounted) {
skipMount, err = mountpoint.IsMounted()
if err != nil {
return fmt.Errorf("mountpoint is set to skip if mounted, but the mount check failed: %w", err)
}
}
if mountpoint.MountFlags.Check(SkipIfNoFilesystem) && mountpoint.Fstype() == filesystem.Unknown {
skipMount = true
}
if !skipMount {
if err = mountpoint.Mount(); err != nil {
return fmt.Errorf("error mounting: %w", err)
}
}
// Grow the filesystem to the maximum allowed size.
//
// Growfs is called always, even if ResizePartition returns false to workaround failure scenario
// when partition was resized, but growfs never got called.
if mountpoint.MountFlags.Check(Resize) {
if err = mountpoint.GrowFilesystem(); err != nil {
return fmt.Errorf("error resizing filesystem: %w", err)
}
}
return nil
}
// Unmount unmounts the device(s).
func Unmount(mountpoints *Points) (err error) {
iter := mountpoints.IterRev()
for iter.Next() {
mountpoint := iter.Value()
if err = mountpoint.Unmount(); err != nil {
return fmt.Errorf("unmount: %w", err)
}
}
if iter.Err() != nil {
return iter.Err()
}
return nil
}
// Move 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 Move(mountpoints *Points, prefix string) (err error) {
iter := mountpoints.Iter()
for iter.Next() {
mountpoint := iter.Value()
if err = mountpoint.Move(prefix); err != nil {
return fmt.Errorf("move: %w", err)
}
}
if iter.Err() != nil {
return iter.Err()
}
return nil
}
// PrefixMountTargets prefixes all mountpoints targets with fixed path.
func PrefixMountTargets(mountpoints *Points, targetPrefix string) error {
iter := mountpoints.Iter()
for iter.Next() {
mountpoint := iter.Value()
mountpoint.target = filepath.Join(targetPrefix, mountpoint.target)
}
return iter.Err()
}
func mountRetry(f RetryFunc, p *Point, isUnmount bool) (err error) {
err = retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error {
if err = f(p); err != nil {
switch err {
case unix.EBUSY:
return retry.ExpectedError(err)
case unix.ENOENT:
// if udevd triggers BLKRRPART ioctl, partition device entry might disappear temporarily
return retry.ExpectedError(err)
case unix.EINVAL:
isMounted, checkErr := p.IsMounted()
if checkErr != nil {
return retry.ExpectedError(checkErr)
}
if !isMounted && isUnmount { // if partition is already unmounted, ignore EINVAL
return nil
}
return err
default:
return err
}
}
return nil
})
return err
}
// Point represents a Linux mount point.
type Point struct {
source string
target string
fstype string
flags uintptr
data string
*Options
}
// PointMap represents a unique set of mount points.
type PointMap = map[string]*Point
// Points represents an ordered set of mount points.
type Points struct {
points PointMap
order []string
}
// NewMountPoint initializes and returns a Point struct.
func NewMountPoint(source, target, fstype string, flags uintptr, data string, setters ...Option) *Point {
opts := NewDefaultOptions(setters...)
return &Point{
source: source,
target: target,
fstype: fstype,
flags: flags,
data: data,
Options: opts,
}
}
// NewMountPoints initializes and returns a Points struct.
func NewMountPoints() *Points {
return &Points{
points: make(PointMap),
}
}
// Source returns the mount points source field.
func (p *Point) Source() string {
return p.source
}
// Target returns the mount points target field.
func (p *Point) Target() string {
return p.target
}
// Fstype returns the mount points fstype field.
func (p *Point) Fstype() string {
return p.fstype
}
// Flags returns the mount points flags field.
func (p *Point) Flags() uintptr {
return p.flags
}
// Data returns the mount points data field.
func (p *Point) Data() string {
return p.data
}
// Mount attempts to retry a mount on EBUSY. It will attempt a retry
// every 100 milliseconds over the course of 5 seconds.
func (p *Point) Mount() (err error) {
p.target = path.Join(p.Prefix, p.target)
for _, hook := range p.Options.PreMountHooks {
if err = hook(p); err != nil {
return err
}
}
if err = ensureDirectory(p.target); err != nil {
return err
}
if p.MountFlags.Check(ReadOnly) {
p.flags |= unix.MS_RDONLY
}
switch {
case p.MountFlags.Check(Overlay):
err = mountRetry(overlay, p, false)
default:
err = mountRetry(mount, p, false)
}
if err != nil {
return err
}
if p.MountFlags.Check(Shared) {
if err = mountRetry(share, p, false); err != nil {
return fmt.Errorf("error sharing mount point %s: %+v", p.target, err)
}
}
return nil
}
// Unmount attempts to retry an unmount on EBUSY. It will attempt a
// retry every 100 milliseconds over the course of 5 seconds.
func (p *Point) Unmount() (err error) {
var mounted bool
if mounted, err = p.IsMounted(); err != nil {
return err
}
if mounted {
p.target = path.Join(p.Prefix, p.target)
if err = mountRetry(unmount, p, true); err != nil {
return err
}
}
for _, hook := range p.Options.PostUnmountHooks {
if err = hook(p); err != nil {
return err
}
}
return nil
}
// IsMounted checks whether mount point is active under /proc/mounts.
func (p *Point) IsMounted() (bool, error) {
f, err := os.Open("/proc/mounts")
if err != nil {
return false, err
}
defer f.Close() //nolint:errcheck
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 2 {
continue
}
mountpoint := fields[1]
if mountpoint == p.target {
return true, nil
}
}
return false, scanner.Err()
}
// 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 fmt.Errorf("error moving mount point %s: %w", target, err)
}
return nil
}
// ResizePartition resizes a partition to the maximum size allowed.
func (p *Point) ResizePartition() (resized bool, err error) {
var devname string
if devname, err = util.DevnameFromPartname(p.Source()); err != nil {
return false, err
}
bd, err := blockdevice.Open("/dev/"+devname, blockdevice.WithExclusiveLock(true))
if err != nil {
return false, fmt.Errorf("error opening block device %q: %w", devname, err)
}
//nolint:errcheck
defer bd.Close()
pt, err := bd.PartitionTable()
if err != nil {
return false, err
}
if err := pt.Repair(); err != nil {
return false, err
}
for _, partition := range pt.Partitions().Items() {
if partition.Name == constants.EphemeralPartitionLabel {
resized, err := pt.Resize(partition)
if err != nil {
return false, err
}
if !resized {
return false, nil
}
}
}
if err := pt.Write(); err != nil {
return false, err
}
return true, 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 = makefs.XFSGrow(p.Target()); err != nil {
return fmt.Errorf("xfs_growfs: %w", err)
}
return nil
}
func mount(p *Point) (err error) {
return unix.Mount(p.source, p.target, p.fstype, p.flags, p.data)
}
func unmount(p *Point) error {
return unix.Unmount(p.target, 0)
}
func share(p *Point) error {
return unix.Mount("", p.target, "", unix.MS_SHARED|unix.MS_REC, "")
}
func overlay(p *Point) error {
parts := strings.Split(p.target, "/")
prefix := strings.Join(parts[1:], "-")
diff := fmt.Sprintf(filepath.Join(constants.SystemOverlaysPath, "%s-diff"), prefix)
workdir := fmt.Sprintf(filepath.Join(constants.SystemOverlaysPath, "%s-workdir"), prefix)
for _, target := range []string{diff, workdir} {
if err := ensureDirectory(target); err != nil {
return err
}
}
opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", p.target, diff, workdir)
if err := unix.Mount("overlay", p.target, "overlay", 0, opts); err != nil {
return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err)
}
return nil
}
func ensureDirectory(target string) (err error) {
if _, err := os.Stat(target); os.IsNotExist(err) {
if err = os.MkdirAll(target, os.ModeDir); err != nil {
return fmt.Errorf("error creating mount point directory %s: %w", target, err)
}
}
return nil
}