Skip to content

Commit

Permalink
Merge pull request #208 from dtrudg/issue205
Browse files Browse the repository at this point in the history
feat: Add experimental unmount command
  • Loading branch information
tri-adam committed Apr 20, 2022
2 parents 98cb26e + dcfb28c commit 3e8948f
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 0 deletions.
20 changes: 20 additions & 0 deletions internal/app/siftool/unmount.go
@@ -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),
)
}
93 changes: 93 additions & 0 deletions internal/pkg/exp/unmount.go
@@ -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)
}
139 changes: 139 additions & 0 deletions internal/pkg/exp/unmount_test.go
@@ -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
}
1 change: 1 addition & 0 deletions pkg/siftool/siftool.go
Expand Up @@ -78,6 +78,7 @@ func AddCommands(cmd *cobra.Command, opts ...CommandOpt) error {

if c.opts.experimental {
cmd.AddCommand(c.getMount())
cmd.AddCommand(c.getUnmount())
}

return nil
Expand Down
Empty file.
Empty file.
27 changes: 27 additions & 0 deletions pkg/siftool/unmount.go
@@ -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
}
}
42 changes: 42 additions & 0 deletions pkg/siftool/unmount_test.go
@@ -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)
}

0 comments on commit 3e8948f

Please sign in to comment.