-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #208 from dtrudg/issue205
feat: Add experimental unmount command
- Loading branch information
Showing
8 changed files
with
322 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" | ||
) | ||
|
||
// Unmounts 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,93 @@ | ||
// 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" | ||
"fmt" | ||
"io" | ||
"os/exec" | ||
"path/filepath" | ||
) | ||
|
||
// unmountSquashFS unmounts the filesystem at mountPath. | ||
func unmountSquashFS(ctx context.Context, mountPath string, uo unmountOpts) error { | ||
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 unmount options. | ||
type unmountOpts struct { | ||
stdout io.Writer | ||
stderr io.Writer | ||
fusermountPath string | ||
} | ||
|
||
// UnmountOpt are used to specify unmount 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 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,139 @@ | ||
// 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" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
"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 bool | ||
wantUnmounted bool | ||
}{ | ||
{ | ||
name: "Mounted", | ||
mountSIF: filepath.Join(corpus, "one-group.sif"), | ||
mountPath: path, | ||
wantErr: false, | ||
wantUnmounted: true, | ||
}, | ||
{ | ||
name: "NotMounted", | ||
mountSIF: "", | ||
mountPath: path, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "NotSquashfuse", | ||
mountSIF: "", | ||
mountPath: "/dev", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "FusermountBare", | ||
mountSIF: "", | ||
mountPath: path, | ||
opts: []UnmountOpt{OptUnmountFusermountPath("fusermount")}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "FusermountValid", | ||
mountSIF: filepath.Join(corpus, "one-group.sif"), | ||
mountPath: path, | ||
opts: []UnmountOpt{OptUnmountFusermountPath(fusermountPath)}, | ||
wantErr: false, | ||
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 err != nil && !tt.wantErr { | ||
t.Errorf("Unexpected error: %s", err) | ||
} | ||
if err == nil && tt.wantErr { | ||
t.Error("Unexpected success") | ||
} | ||
|
||
mounted, err := isMounted(tt.mountPath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if tt.wantUnmounted && mounted { | ||
t.Errorf("Expected %s to be unmounted, but it is mounted", tt.mountPath) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
var errBadMountInfo = errors.New("bad mount info") | ||
|
||
func isMounted(mountPath string) (bool, error) { | ||
mountPath, err := filepath.Abs(mountPath) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
mi, err := os.Open("/proc/self/mountinfo") | ||
if err != nil { | ||
return false, fmt.Errorf("failed to open /proc/self/mountinfo: %w", err) | ||
} | ||
defer mi.Close() | ||
|
||
scanner := bufio.NewScanner(mi) | ||
for scanner.Scan() { | ||
fields := strings.Split(scanner.Text(), " ") | ||
if len(fields) < 5 { | ||
return false, fmt.Errorf("not enough mountinfo fields: %w", 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] | ||
if mntTarget == mountPath { | ||
return true, nil | ||
} | ||
} | ||
return false, nil | ||
} |
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 unmount tests") | ||
} | ||
if _, err := exec.LookPath("fusermount"); err != nil { | ||
t.Skip(" not found, skipping unmount tests") | ||
} | ||
|
||
path, err := os.MkdirTemp("", "siftool-unmount-*") | ||
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) | ||
} |