-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add experimental umount command
Counterpart to mount, will unmount a squashfuse mounted filesystem via fusermount. Fixes #205
- Loading branch information
Showing
11 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) 2022, Sylabs Inc. All rights reserved. | ||
// This software is licensed under a 3-clause BSD license. Please consult the | ||
// LICENSE file distributed with the sources of this project regarding your | ||
// rights to use or distribute this software. | ||
|
||
package siftool | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/sylabs/sif/v2/internal/pkg/exp" | ||
) | ||
|
||
// Umount umounts the FUSE mounted filesystem at mountPath. | ||
func (a *App) Umount(ctx context.Context, mountPath string) error { | ||
return exp.Umount(ctx, mountPath, | ||
exp.OptUmountStdout(a.opts.out), | ||
exp.OptUmountStderr(a.opts.err), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright (c) 2022, Sylabs Inc. All rights reserved. | ||
// This software is licensed under a 3-clause BSD license. Please consult the | ||
// LICENSE file distributed with the sources of this project regarding your | ||
// rights to use or distribute this software. | ||
|
||
// Package exp contains experimental functionality that is not sufficiently mature to be exported | ||
// as part of the module API. | ||
package exp | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
) | ||
|
||
const mountInfoPath = "/proc/self/mountinfo" | ||
|
||
// ErrNotMounted is the error returned when attempting to unmount a path that | ||
// has no mount associated with it. | ||
var ErrNotMounted = errors.New("not mounted") | ||
|
||
// ErrNotSquashfuse is the error returned when attempting to unmount a path that | ||
// is not a squashfuse mount. | ||
var ErrNotSquashfuse = errors.New("not a squashfuse mount") | ||
|
||
// ErrBadMountInfo is the error returned if we cannot parse /proc/self/mountinfo. | ||
var ErrBadMountInfo = errors.New("bad mountinfo") | ||
|
||
// checkMounted verifies whether mountPath is a current squashfuse mount. | ||
func checkMounted(mountPath string) error { | ||
mountPath, err := filepath.Abs(mountPath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
mi, err := os.Open("/proc/self/mountinfo") | ||
if err != nil { | ||
return fmt.Errorf("failed to open %s: %w", mountInfoPath, err) | ||
} | ||
defer mi.Close() | ||
|
||
scanner := bufio.NewScanner(mi) | ||
for scanner.Scan() { | ||
fields := strings.Split(scanner.Text(), " ") | ||
if len(fields) < 10 { | ||
return fmt.Errorf("%w: not enough fields", ErrBadMountInfo) | ||
} | ||
//nolint:lll | ||
// 1348 63 0:77 / /tmp/siftool-mount-956028386 ro,nosuid,nodev,relatime shared:646 - fuse.squashfuse squashfuse ro,user_id=1000,group_id=100 | ||
mntTarget := fields[4] | ||
// Number of fields is not fixed - so loop over field 7+ | ||
if mntTarget == mountPath { | ||
for _, v := range fields[6:] { | ||
if v == "squashfuse" { | ||
return nil | ||
} | ||
} | ||
return ErrNotSquashfuse | ||
} | ||
} | ||
return ErrNotMounted | ||
} | ||
|
||
// umountSquashFS unmounts the filesystem at mountPath. | ||
func umountSquashFS(ctx context.Context, mountPath string, uo umountOpts) error { | ||
if err := checkMounted(mountPath); err != nil { | ||
return err | ||
} | ||
|
||
args := []string{ | ||
"-u", | ||
filepath.Clean(mountPath), | ||
} | ||
cmd := exec.CommandContext(ctx, uo.fusermountPath, args...) //nolint:gosec | ||
cmd.Stdout = uo.stdout | ||
cmd.Stderr = uo.stderr | ||
|
||
if err := cmd.Run(); err != nil { | ||
return fmt.Errorf("failed to umount: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// mountOpts accumulates mount options. | ||
type umountOpts struct { | ||
stdout io.Writer | ||
stderr io.Writer | ||
fusermountPath string | ||
} | ||
|
||
// MountOpt are used to specify mount options. | ||
type UmountOpt func(*umountOpts) error | ||
|
||
// OptMountStdout writes standard output to w. | ||
func OptUmountStdout(w io.Writer) UmountOpt { | ||
return func(mo *umountOpts) error { | ||
mo.stdout = w | ||
return nil | ||
} | ||
} | ||
|
||
// OptMountStderr writes standard error to w. | ||
func OptUmountStderr(w io.Writer) UmountOpt { | ||
return func(mo *umountOpts) error { | ||
mo.stderr = w | ||
return nil | ||
} | ||
} | ||
|
||
// OptMountFusermountPath sets the path to the fusermount binary. | ||
func OptUmountFusermount(path string) UmountOpt { | ||
return func(mo *umountOpts) error { | ||
mo.fusermountPath = path | ||
return nil | ||
} | ||
} | ||
|
||
// Umount unmounts the FUSE mounted filesystem at mountPath. | ||
// | ||
// Umount may start one or more underlying processes. By default, stdout and stderr of these | ||
// processes is discarded. To modify this behavior, consider using OptUmountStdout and/or | ||
// OptUmountStderr. | ||
func Umount(ctx context.Context, mountPath string, opts ...UmountOpt) error { | ||
uo := umountOpts{ | ||
// Default to searching for fusermount on PATH | ||
fusermountPath: "fusermount", | ||
} | ||
|
||
for _, opt := range opts { | ||
if err := opt(&uo); err != nil { | ||
return fmt.Errorf("%w", err) | ||
} | ||
} | ||
|
||
return umountSquashFS(ctx, mountPath, uo) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
1 change: 1 addition & 0 deletions
1
pkg/siftool/testdata/Test_command_getUmount/NotMounted/err.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Error: not mounted |
9 changes: 9 additions & 0 deletions
9
pkg/siftool/testdata/Test_command_getUmount/NotMounted/out.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Usage: | ||
umount <mount_path> | ||
|
||
Examples: | ||
umount path/ | ||
|
||
Flags: | ||
-h, --help help for umount | ||
|
1 change: 1 addition & 0 deletions
1
pkg/siftool/testdata/Test_command_getUmount/NotSquashfuse/err.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Error: not a squashfuse mount |
9 changes: 9 additions & 0 deletions
9
pkg/siftool/testdata/Test_command_getUmount/NotSquashfuse/out.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Usage: | ||
umount <mount_path> | ||
|
||
Examples: | ||
umount path/ | ||
|
||
Flags: | ||
-h, --help help for umount | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Copyright (c) 2022, Sylabs Inc. All rights reserved. | ||
// This software is licensed under a 3-clause BSD license. Please consult the | ||
// LICENSE file distributed with the sources of this project regarding your | ||
// rights to use or distribute this software. | ||
|
||
package siftool | ||
|
||
import ( | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
// getMount returns a command that mounts the primary system partition of a SIF image. | ||
func (c *command) getUmount() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "umount <mount_path>", | ||
Short: "Unmount primary system partition", | ||
Long: "Unmount a primary system partition of a SIF image", | ||
Example: c.opts.rootPath + " umount path/", | ||
Args: cobra.ExactArgs(1), | ||
PreRunE: c.initApp, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return c.app.Umount(cmd.Context(), args[0]) | ||
}, | ||
DisableFlagsInUseLine: true, | ||
Hidden: true, // hide while command is experimental | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// Copyright (c) 2022, Sylabs Inc. All rights reserved. | ||
// This software is licensed under a 3-clause BSD license. Please consult the | ||
// LICENSE file distributed with the sources of this project regarding your | ||
// rights to use or distribute this software. | ||
|
||
package siftool | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/sylabs/sif/v2/internal/pkg/exp" | ||
) | ||
|
||
func Test_command_getUmount(t *testing.T) { | ||
if _, err := exec.LookPath("squashfuse"); err != nil { | ||
t.Skip(" not found, skipping mount tests") | ||
} | ||
if _, err := exec.LookPath("fusermount"); err != nil { | ||
t.Skip(" not found, skipping mount tests") | ||
} | ||
|
||
path, err := os.MkdirTemp("", "siftool-mount-*") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
t.Cleanup(func() { | ||
os.RemoveAll(path) | ||
}) | ||
|
||
tests := []struct { | ||
name string | ||
mountSIF string | ||
mountPath string | ||
opts commandOpts | ||
wantErr error | ||
}{ | ||
{ | ||
name: "Mounted", | ||
mountSIF: filepath.Join(corpus, "one-group.sif"), | ||
mountPath: path, | ||
}, | ||
{ | ||
name: "NotMounted", | ||
mountSIF: "", | ||
wantErr: exp.ErrNotMounted, | ||
mountPath: path, | ||
}, | ||
{ | ||
name: "NotSquashfuse", | ||
mountSIF: "", | ||
wantErr: exp.ErrNotSquashfuse, | ||
mountPath: "/dev", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if tt.mountSIF != "" { | ||
err := exp.Mount(context.Background(), tt.mountSIF, path) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
} | ||
c := &command{opts: tt.opts} | ||
|
||
cmd := c.getUmount() | ||
|
||
runCommand(t, cmd, []string{tt.mountPath}, tt.wantErr) | ||
}) | ||
} | ||
} |