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 #205
  • Loading branch information
dtrudg committed Apr 18, 2022
1 parent 5314bc0 commit 561b25b
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 0 deletions.
20 changes: 20 additions & 0 deletions internal/app/siftool/umount.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"
)

// Umount umounts the FUSE mounted filesystem at mountPath.
func (a *App) Umount(ctx context.Context, mountPath string) error {
return exp.Umount(ctx, mountPath,
exp.OptUmountStdout(a.opts.out),
exp.OptUmountStderr(a.opts.err),
)
}
143 changes: 143 additions & 0 deletions internal/pkg/exp/umount.go
@@ -0,0 +1,143 @@
// 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 contains experimental functionality that is not sufficiently mature to be exported
// as part of the module API.
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
}

// umountSquashFS unmounts the filesystem at mountPath.
func umountSquashFS(ctx context.Context, mountPath string, uo umountOpts) 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 umount: %w", err)
}

return nil
}

// mountOpts accumulates mount options.
type umountOpts struct {
stdout io.Writer
stderr io.Writer
fusermountPath string
}

// MountOpt are used to specify mount options.
type UmountOpt func(*umountOpts) error

// OptMountStdout writes standard output to w.
func OptUmountStdout(w io.Writer) UmountOpt {
return func(mo *umountOpts) error {
mo.stdout = w
return nil
}
}

// OptMountStderr writes standard error to w.
func OptUmountStderr(w io.Writer) UmountOpt {
return func(mo *umountOpts) error {
mo.stderr = w
return nil
}
}

// OptMountFusermountPath sets the path to the fusermount binary.
func OptUmountFusermount(path string) UmountOpt {
return func(mo *umountOpts) error {
mo.fusermountPath = path
return nil
}
}

// Umount unmounts the FUSE mounted filesystem at mountPath.
//
// Umount may start one or more underlying processes. By default, stdout and stderr of these
// processes is discarded. To modify this behavior, consider using OptUmountStdout and/or
// OptUmountStderr.
func Umount(ctx context.Context, mountPath string, opts ...UmountOpt) error {
uo := umountOpts{
// Default to searching for fusermount on PATH
fusermountPath: "fusermount",
}

for _, opt := range opts {
if err := opt(&uo); err != nil {
return fmt.Errorf("%w", err)
}
}

return umountSquashFS(ctx, mountPath, uo)
}
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.getUmount())
}

return nil
Expand Down
Empty file.
Empty file.
@@ -0,0 +1 @@
Error: not mounted
@@ -0,0 +1,9 @@
Usage:
umount <mount_path>

Examples:
umount path/

Flags:
-h, --help help for umount

@@ -0,0 +1 @@
Error: not a squashfuse mount
@@ -0,0 +1,9 @@
Usage:
umount <mount_path>

Examples:
umount path/

Flags:
-h, --help help for umount

27 changes: 27 additions & 0 deletions pkg/siftool/umount.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"
)

// getMount returns a command that mounts the primary system partition of a SIF image.
func (c *command) getUmount() *cobra.Command {
return &cobra.Command{
Use: "umount <mount_path>",
Short: "Unmount primary system partition",
Long: "Unmount a primary system partition of a SIF image",
Example: c.opts.rootPath + " umount path/",
Args: cobra.ExactArgs(1),
PreRunE: c.initApp,
RunE: func(cmd *cobra.Command, args []string) error {
return c.app.Umount(cmd.Context(), args[0])
},
DisableFlagsInUseLine: true,
Hidden: true, // hide while command is experimental
}
}
74 changes: 74 additions & 0 deletions pkg/siftool/umount_test.go
@@ -0,0 +1,74 @@
// 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_getUmount(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)
})

tests := []struct {
name string
mountSIF string
mountPath string
opts commandOpts
wantErr error
}{
{
name: "Mounted",
mountSIF: filepath.Join(corpus, "one-group.sif"),
mountPath: path,
},
{
name: "NotMounted",
mountSIF: "",
wantErr: exp.ErrNotMounted,
mountPath: path,
},
{
name: "NotSquashfuse",
mountSIF: "",
wantErr: exp.ErrNotSquashfuse,
mountPath: "/dev",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.mountSIF != "" {
err := exp.Mount(context.Background(), tt.mountSIF, path)
if err != nil {
t.Fatal(err)
}
}
c := &command{opts: tt.opts}

cmd := c.getUmount()

runCommand(t, cmd, []string{tt.mountPath}, tt.wantErr)
})
}
}

0 comments on commit 561b25b

Please sign in to comment.