1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
17
"github.com/siderolabs/go-blockdevice/blockdevice"
18
"github.com/siderolabs/go-blockdevice/blockdevice/filesystem"
19
"github.com/siderolabs/go-blockdevice/blockdevice/util"
20
"github.com/siderolabs/go-retry/retry"
21
"golang.org/x/sys/unix"
23
"github.com/siderolabs/talos/pkg/machinery/constants"
24
"github.com/siderolabs/talos/pkg/makefs"
27
// RetryFunc defines the requirements for retrying a mount point operation.
28
type RetryFunc func(*Point) error
30
// Mount mounts the device(s).
31
func Mount(mountpoints *Points) (err error) {
32
iter := mountpoints.Iter()
34
// Mount the device(s).
37
if _, err = mountMountpoint(iter.Value()); err != nil {
38
return fmt.Errorf("error mounting %q: %w", iter.Value().Source(), err)
42
if iter.Err() != nil {
50
func mountMountpoint(mountpoint *Point) (skipMount bool, err error) {
51
// Repair the disk's partition table.
52
if mountpoint.MountFlags.Check(Resize) {
53
if _, err = mountpoint.ResizePartition(); err != nil {
54
return false, fmt.Errorf("error resizing %w", err)
58
if mountpoint.MountFlags.Check(SkipIfMounted) {
59
skipMount, err = mountpoint.IsMounted()
61
return false, fmt.Errorf("mountpoint is set to skip if mounted, but the mount check failed: %w", err)
65
if mountpoint.MountFlags.Check(SkipIfNoFilesystem) && mountpoint.Fstype() == filesystem.Unknown {
70
if err = mountpoint.Mount(); err != nil {
71
if mountpoint.MountFlags.Check(SkipIfNoDevice) && errors.Is(err, unix.ENODEV) {
72
if mountpoint.Logger != nil {
73
mountpoint.Logger.Printf("error mounting: %q: %s", mountpoint.Source(), err)
79
return false, fmt.Errorf("error mounting: %w", err)
83
// Grow the filesystem to the maximum allowed size.
85
// Growfs is called always, even if ResizePartition returns false to workaround failure scenario
86
// when partition was resized, but growfs never got called.
87
if mountpoint.MountFlags.Check(Resize) {
88
if err = mountpoint.GrowFilesystem(); err != nil {
89
return false, fmt.Errorf("error resizing filesystem: %w", err)
96
// Unmount unmounts the device(s).
97
func Unmount(mountpoints *Points) (err error) {
98
iter := mountpoints.IterRev()
100
mountpoint := iter.Value()
101
if err = mountpoint.Unmount(); err != nil {
102
return fmt.Errorf("unmount: %w", err)
106
if iter.Err() != nil {
113
// Move moves the device(s).
114
// TODO(andrewrynhard): We need to skip calling the move method on mountpoints
115
// that are a child of another mountpoint. The kernel will handle moving the
116
// child mountpoints for us.
117
func Move(mountpoints *Points, prefix string) (err error) {
118
iter := mountpoints.Iter()
120
mountpoint := iter.Value()
121
if err = mountpoint.Move(prefix); err != nil {
122
return fmt.Errorf("move: %w", err)
126
if iter.Err() != nil {
133
// PrefixMountTargets prefixes all mountpoints targets with fixed path.
134
func PrefixMountTargets(mountpoints *Points, targetPrefix string) error {
135
iter := mountpoints.Iter()
137
mountpoint := iter.Value()
138
mountpoint.target = filepath.Join(targetPrefix, mountpoint.target)
145
func mountRetry(f RetryFunc, p *Point, isUnmount bool) (err error) {
146
err = retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error {
147
if err = f(p); err != nil {
150
return retry.ExpectedError(err)
151
case unix.ENOENT, unix.ENODEV:
152
// if udevd triggers BLKRRPART ioctl, partition device entry might disappear temporarily
153
return retry.ExpectedError(err)
155
if errRepair := p.Repair(); errRepair != nil {
156
return fmt.Errorf("error repairing: %w", errRepair)
159
return retry.ExpectedError(err)
161
isMounted, checkErr := p.IsMounted()
163
return retry.ExpectedError(checkErr)
166
if !isMounted && !isUnmount {
167
if errRepair := p.Repair(); errRepair != nil {
168
return fmt.Errorf("error repairing: %w", errRepair)
171
return retry.ExpectedError(err)
174
if !isMounted && isUnmount { // if partition is already unmounted, ignore EINVAL
190
// Point represents a Linux mount point.
200
// PointMap represents a unique set of mount points.
201
type PointMap = map[string]*Point
203
// Points represents an ordered set of mount points.
209
// NewMountPoint initializes and returns a Point struct.
210
func NewMountPoint(source, target, fstype string, flags uintptr, data string, setters ...Option) *Point {
211
opts := NewDefaultOptions(setters...)
223
p.target = filepath.Join(p.Prefix, p.target)
226
if p.Options.ProjectQuota {
237
// NewMountPoints initializes and returns a Points struct.
238
func NewMountPoints() *Points {
240
points: make(PointMap),
244
// Source returns the mount points source field.
245
func (p *Point) Source() string {
249
// Target returns the mount points target field.
250
func (p *Point) Target() string {
254
// Fstype returns the mount points fstype field.
255
func (p *Point) Fstype() string {
259
// Flags returns the mount points flags field.
260
func (p *Point) Flags() uintptr {
264
// Data returns the mount points data field.
265
func (p *Point) Data() string {
269
// Mount attempts to retry a mount on EBUSY. It will attempt a retry
270
// every 100 milliseconds over the course of 5 seconds.
271
func (p *Point) Mount() (err error) {
272
for _, hook := range p.Options.PreMountHooks {
273
if err = hook(p); err != nil {
278
if err = ensureDirectory(p.target); err != nil {
282
if p.MountFlags.Check(ReadOnly) {
283
p.flags |= unix.MS_RDONLY
287
case p.MountFlags.Check(Overlay):
288
err = mountRetry(overlay, p, false)
289
case p.MountFlags.Check(ReadonlyOverlay):
290
err = mountRetry(readonlyOverlay, p, false)
292
err = mountRetry(mount, p, false)
299
if p.MountFlags.Check(Shared) {
300
if err = mountRetry(share, p, false); err != nil {
301
return fmt.Errorf("error sharing mount point %s: %+v", p.target, err)
308
// Unmount attempts to retry an unmount on EBUSY. It will attempt a
309
// retry every 100 milliseconds over the course of 5 seconds.
310
func (p *Point) Unmount() (err error) {
313
if mounted, err = p.IsMounted(); err != nil {
318
if err = mountRetry(unmount, p, true); err != nil {
323
for _, hook := range p.Options.PostUnmountHooks {
324
if err = hook(p); err != nil {
332
// IsMounted checks whether mount point is active under /proc/mounts.
333
func (p *Point) IsMounted() (bool, error) {
334
f, err := os.Open("/proc/mounts")
339
defer f.Close() //nolint:errcheck
341
scanner := bufio.NewScanner(f)
343
fields := strings.Fields(scanner.Text())
349
mountpoint := fields[1]
351
if mountpoint == p.target {
356
return false, scanner.Err()
359
// Move moves a mountpoint to a new location with a prefix.
360
func (p *Point) Move(prefix string) (err error) {
362
mountpoint := NewMountPoint(target, target, "", unix.MS_MOVE, "", WithPrefix(prefix))
364
if err = mountpoint.Mount(); err != nil {
365
return fmt.Errorf("error moving mount point %s: %w", target, err)
371
// ResizePartition resizes a partition to the maximum size allowed.
372
func (p *Point) ResizePartition() (resized bool, err error) {
375
if devname, err = util.DevnameFromPartname(p.Source()); err != nil {
379
bd, err := blockdevice.Open("/dev/"+devname, blockdevice.WithExclusiveLock(true))
381
return false, fmt.Errorf("error opening block device %q: %w", devname, err)
387
pt, err := bd.PartitionTable()
392
if err := pt.Repair(); err != nil {
396
for _, partition := range pt.Partitions().Items() {
397
if partition.Name == constants.EphemeralPartitionLabel {
398
resized, err := pt.Resize(partition)
409
if err := pt.Write(); err != nil {
416
// GrowFilesystem grows a partition's filesystem to the maximum size allowed.
417
// NB: An XFS partition MUST be mounted, or this will fail.
418
func (p *Point) GrowFilesystem() (err error) {
419
if err = makefs.XFSGrow(p.Target()); err != nil {
420
return fmt.Errorf("xfs_growfs: %w", err)
426
// Repair repairs a partition's filesystem.
427
func (p *Point) Repair() error {
429
p.Logger.Printf("filesystem on %s needs cleaning, running repair", p.Source())
432
if err := makefs.XFSRepair(p.Source(), p.Fstype()); err != nil {
433
return fmt.Errorf("xfs_repair: %w", err)
437
p.Logger.Printf("filesystem successfully repaired on %s", p.Source())
443
func mount(p *Point) (err error) {
444
return unix.Mount(p.source, p.target, p.fstype, p.flags, p.data)
447
func unmount(p *Point) error {
448
return SafeUnmount(context.Background(), p.Logger, p.target)
451
func share(p *Point) error {
452
return unix.Mount("", p.target, "", unix.MS_SHARED|unix.MS_REC, "")
455
func overlay(p *Point) error {
456
parts := strings.Split(p.target, "/")
457
prefix := strings.Join(parts[1:], "-")
459
basePath := constants.VarSystemOverlaysPath
461
if p.MountFlags.Check(SystemOverlay) {
462
basePath = constants.SystemOverlaysPath
465
diff := fmt.Sprintf(filepath.Join(basePath, "%s-diff"), prefix)
466
workdir := fmt.Sprintf(filepath.Join(basePath, "%s-workdir"), prefix)
468
for _, target := range []string{diff, workdir} {
469
if err := ensureDirectory(target); err != nil {
479
opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, diff, workdir)
480
if err := unix.Mount("overlay", p.target, "overlay", 0, opts); err != nil {
481
return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err)
487
func readonlyOverlay(p *Point) error {
488
opts := fmt.Sprintf("lowerdir=%s", p.source)
489
if err := unix.Mount("overlay", p.target, "overlay", p.flags, opts); err != nil {
490
return fmt.Errorf("error creating overlay mount to %s: %w", p.target, err)
496
func ensureDirectory(target string) (err error) {
497
if _, err := os.Stat(target); os.IsNotExist(err) {
498
if err = os.MkdirAll(target, 0o755); err != nil {
499
return fmt.Errorf("error creating mount point directory %s: %w", target, err)