Skip to content

Commit

Permalink
feat: Add experimental umount command
Browse files Browse the repository at this point in the history
Counterpart to mount, will unmount a squashfuse mounted filesystem via
fusermount.

Fixes sylabs#205
  • Loading branch information
dtrudg committed Apr 20, 2022
1 parent 3e8948f commit fb9b90b
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 0 deletions.
93 changes: 93 additions & 0 deletions internal/pkg/exp/umount.go
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)
}
139 changes: 139 additions & 0 deletions internal/pkg/exp/umount_test.go
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
}

0 comments on commit fb9b90b

Please sign in to comment.