forked from sylabs/sif
-
Notifications
You must be signed in to change notification settings - Fork 0
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 sylabs#205
- Loading branch information
Showing
8 changed files
with
336 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" | ||
) | ||
|
||
// Umounts the FUSE mounted filesystem at mountPath. | ||
func (a *App) Unmount(ctx context.Context, mountPath string) error { | ||
return exp.Unmount(ctx, mountPath, | ||
exp.OptUnmountStdout(a.opts.out), | ||
exp.OptUnmountStderr(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,148 @@ | ||
// 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 | ||
|
||
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 | ||
} | ||
|
||
// unmountSquashFS unmounts the filesystem at mountPath. | ||
func unmountSquashFS(ctx context.Context, mountPath string, uo unmountOpts) 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 unmount: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// unmountOpts accumulates mount options. | ||
type unmountOpts struct { | ||
stdout io.Writer | ||
stderr io.Writer | ||
fusermountPath string | ||
} | ||
|
||
// UnmountOpt are used to specify mount options. | ||
type UnmountOpt func(*unmountOpts) error | ||
|
||
// OptUnmountStdout writes standard output to w. | ||
func OptUnmountStdout(w io.Writer) UnmountOpt { | ||
return func(mo *unmountOpts) error { | ||
mo.stdout = w | ||
return nil | ||
} | ||
} | ||
|
||
// OptUnmountStderr writes standard error to w. | ||
func OptUnmountStderr(w io.Writer) UnmountOpt { | ||
return func(mo *unmountOpts) error { | ||
mo.stderr = w | ||
return nil | ||
} | ||
} | ||
|
||
var errFusermountPathInvalid = errors.New("fusermount path must be relative or absolute") | ||
|
||
// OptUnmountFusermountPath sets the path to the fusermount binary. | ||
func OptUnmountFusermountPath(path string) UnmountOpt { | ||
return func(mo *unmountOpts) error { | ||
if filepath.Base(path) == path { | ||
return errFusermountPathInvalid | ||
} | ||
mo.fusermountPath = path | ||
return nil | ||
} | ||
} | ||
|
||
// Unmount unmounts the FUSE mounted filesystem at mountPath. | ||
// | ||
// Unmount may start one or more underlying processes. By default, stdout and stderr of these | ||
// processes is discarded. To modify this behavior, consider using OptUnmountStdout and/or | ||
// OptUnmountStderr. | ||
// | ||
// By default, Unmount searches for a fusermount binary in the directories named by the PATH | ||
// environment variable. To override this behavior, consider using OptUnmountFusermountPath(). | ||
func Unmount(ctx context.Context, mountPath string, opts ...UnmountOpt) error { | ||
uo := unmountOpts{ | ||
fusermountPath: "fusermount", | ||
} | ||
|
||
for _, opt := range opts { | ||
if err := opt(&uo); err != nil { | ||
return fmt.Errorf("%w", err) | ||
} | ||
} | ||
|
||
return unmountSquashFS(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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// 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 | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"testing" | ||
) | ||
|
||
var corpus = filepath.Join("..", "..", "..", "test", "images") | ||
|
||
func Test_Unmount(t *testing.T) { | ||
if _, err := exec.LookPath("squashfuse"); err != nil { | ||
t.Skip(" not found, skipping mount tests") | ||
} | ||
fusermountPath, err := exec.LookPath("fusermount") | ||
if 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 []UnmountOpt | ||
wantErr error | ||
wantUnmounted bool | ||
}{ | ||
{ | ||
name: "Mounted", | ||
mountSIF: filepath.Join(corpus, "one-group.sif"), | ||
mountPath: path, | ||
wantUnmounted: true, | ||
}, | ||
{ | ||
name: "NotMounted", | ||
mountSIF: "", | ||
mountPath: path, | ||
wantErr: errNotMounted, | ||
}, | ||
{ | ||
name: "NotSquashfuse", | ||
mountSIF: "", | ||
mountPath: "/dev", | ||
wantErr: errNotSquashfuse, | ||
}, | ||
{ | ||
name: "FusermountBare", | ||
mountSIF: "", | ||
mountPath: path, | ||
opts: []UnmountOpt{OptUnmountFusermountPath("bare")}, | ||
wantErr: errFusermountPathInvalid, | ||
}, | ||
{ | ||
name: "FusermountValid", | ||
mountSIF: filepath.Join(corpus, "one-group.sif"), | ||
mountPath: path, | ||
opts: []UnmountOpt{OptUnmountFusermountPath(fusermountPath)}, | ||
wantUnmounted: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if tt.mountSIF != "" { | ||
err := Mount(context.Background(), tt.mountSIF, path) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
} | ||
|
||
err := Unmount(context.Background(), tt.mountPath, tt.opts...) | ||
|
||
if !errors.Is(err, tt.wantErr) { | ||
t.Errorf("Expected err %s, but got %s", tt.wantErr, err) | ||
} | ||
|
||
err = checkMounted(tt.mountPath) | ||
if tt.wantUnmounted && !errors.Is(err, errNotMounted) { | ||
t.Errorf("Expected %s to be unmounted, but it is mounted", tt.mountPath) | ||
} | ||
}) | ||
} | ||
} |
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.
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" | ||
) | ||
|
||
// getUnmount returns a command that unmounts the primary system partition of a SIF image. | ||
func (c *command) getUnmount() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "unmount <mount_path>", | ||
Short: "Unmount primary system partition", | ||
Long: "Unmount a primary system partition of a SIF image", | ||
Example: c.opts.rootPath + " unmount path/", | ||
Args: cobra.ExactArgs(1), | ||
PreRunE: c.initApp, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return c.app.Unmount(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,42 @@ | ||
// 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_getUnmount(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) | ||
}) | ||
|
||
testSIF := filepath.Join(corpus, "one-group.sif") | ||
if err := exp.Mount(context.Background(), testSIF, path); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
c := &command{} | ||
cmd := c.getUnmount() | ||
runCommand(t, cmd, []string{path}, nil) | ||
} |