Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add experimental unmount command #208

Merged
merged 1 commit into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions internal/app/siftool/unmount.go
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),
)
}
93 changes: 93 additions & 0 deletions internal/pkg/exp/unmount.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/unmount_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
}
1 change: 1 addition & 0 deletions pkg/siftool/siftool.go
Original file line number Diff line number Diff line change
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
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
}
}
42 changes: 42 additions & 0 deletions pkg/siftool/unmount_test.go
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)
}