Skip to content

Commit a6a5995

Browse files
committed
Add mount command for mounting machine directories
This comes in handy, when wanting to use volumes with drivers that don't support folder sharing (such as KVM, as opposed to VirtualBox) You will need FUSE and SSHFS installed, in order to use this feature. The machine will also need a "sftp" binary, but most of them have it. Signed-off-by: Anders F Björklund <[email protected]> (cherry picked from commit 30de5bb)
1 parent b6d7614 commit a6a5995

File tree

4 files changed

+234
-0
lines changed

4 files changed

+234
-0
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ FROM golang:1.8.3
33
RUN apt-get update && apt-get install -y --no-install-recommends \
44
openssh-client \
55
rsync \
6+
fuse \
7+
sshfs \
68
&& rm -rf /var/lib/apt/lists/*
79

810
RUN go get github.com/golang/lint/golint \

commands/commands.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,18 @@ var Commands = []cli.Command{
361361
},
362362
},
363363
},
364+
{
365+
Name: "mount",
366+
Usage: "Mount or unmount a directory from a machine with SSHFS.",
367+
Description: "Arguments are [machine:][path] [mountpoint]",
368+
Action: runCommand(cmdMount),
369+
Flags: []cli.Flag{
370+
cli.BoolFlag{
371+
Name: "unmount, u",
372+
Usage: "Unmount instead of mount",
373+
},
374+
},
375+
},
364376
{
365377
Name: "start",
366378
Usage: "Start a machine",

commands/mount.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package commands
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/docker/machine/libmachine"
11+
"github.com/docker/machine/libmachine/log"
12+
)
13+
14+
var (
15+
// TODO: possibly move this to ssh package
16+
baseSSHFSArgs = []string{
17+
"-o", "StrictHostKeyChecking=no",
18+
"-o", "UserKnownHostsFile=/dev/null",
19+
"-o", "LogLevel=quiet", // suppress "Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts."
20+
}
21+
)
22+
23+
func cmdMount(c CommandLine, api libmachine.API) error {
24+
args := c.Args()
25+
if len(args) < 1 || len(args) > 2 {
26+
c.ShowHelp()
27+
return errWrongNumberArguments
28+
}
29+
30+
src := args[0]
31+
dest := ""
32+
if len(args) > 1 {
33+
dest = args[1]
34+
}
35+
36+
hostInfoLoader := &storeHostInfoLoader{api}
37+
38+
cmd, err := getMountCmd(src, dest, c.Bool("unmount"), hostInfoLoader)
39+
if err != nil {
40+
return err
41+
}
42+
43+
cmd.Stdin = os.Stdin
44+
cmd.Stdout = os.Stdout
45+
cmd.Stderr = os.Stderr
46+
47+
return cmd.Run()
48+
}
49+
50+
func getMountCmd(src, dest string, unmount bool, hostInfoLoader HostInfoLoader) (*exec.Cmd, error) {
51+
var cmdPath string
52+
var err error
53+
if !unmount {
54+
cmdPath, err = exec.LookPath("sshfs")
55+
if err != nil {
56+
return nil, errors.New("You must have a copy of the sshfs binary locally to use the mount feature")
57+
}
58+
} else {
59+
cmdPath, err = exec.LookPath("fusermount")
60+
if err != nil {
61+
return nil, errors.New("You must have a copy of the fusermount binary locally to use the unmount option")
62+
}
63+
}
64+
65+
srcHost, srcUser, srcPath, srcOpts, err := getInfoForSshfsArg(src, hostInfoLoader)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
if dest == "" {
71+
dest = srcPath
72+
}
73+
74+
sshArgs := baseSSHFSArgs
75+
if srcHost.GetSSHKeyPath() != "" {
76+
sshArgs = append(sshArgs, "-o", "IdentitiesOnly=yes")
77+
}
78+
79+
// Append needed -i / private key flags to command.
80+
sshArgs = append(sshArgs, srcOpts...)
81+
82+
// Append actual arguments for the sshfs command (i.e. docker@<ip>:/path)
83+
locationArg, err := generateLocationArg(srcHost, srcUser, srcPath)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
if !unmount {
89+
sshArgs = append(sshArgs, locationArg)
90+
sshArgs = append(sshArgs, dest)
91+
} else {
92+
sshArgs = []string{"-u"}
93+
sshArgs = append(sshArgs, dest)
94+
}
95+
96+
cmd := exec.Command(cmdPath, sshArgs...)
97+
log.Debug(*cmd)
98+
return cmd, nil
99+
}
100+
101+
func getInfoForSshfsArg(hostAndPath string, hostInfoLoader HostInfoLoader) (h HostInfo, user string, path string, args []string, err error) {
102+
// Path with hostname. e.g. "hostname:/usr/bin/cmatrix"
103+
var hostName string
104+
if parts := strings.SplitN(hostAndPath, ":", 2); len(parts) < 2 {
105+
hostName = defaultMachineName
106+
path = parts[0]
107+
} else {
108+
hostName = parts[0]
109+
path = parts[1]
110+
}
111+
if hParts := strings.SplitN(hostName, "@", 2); len(hParts) == 2 {
112+
user, hostName = hParts[0], hParts[1]
113+
}
114+
115+
// Remote path
116+
h, err = hostInfoLoader.load(hostName)
117+
if err != nil {
118+
return nil, "", "", nil, fmt.Errorf("Error loading host: %s", err)
119+
}
120+
121+
args = []string{}
122+
port, err := h.GetSSHPort()
123+
if err == nil && port > 0 {
124+
args = append(args, "-o", fmt.Sprintf("Port=%v", port))
125+
}
126+
127+
if h.GetSSHKeyPath() != "" {
128+
args = append(args, "-o", fmt.Sprintf("IdentityFile=%s", h.GetSSHKeyPath()))
129+
}
130+
131+
if user == "" {
132+
user = h.GetSSHUsername()
133+
}
134+
135+
return
136+
}

commands/mount_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package commands
2+
3+
import (
4+
"os/exec"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGetMountCmd(t *testing.T) {
11+
hostInfoLoader := MockHostInfoLoader{MockHostInfo{
12+
ip: "12.34.56.78",
13+
sshPort: 234,
14+
sshUsername: "root",
15+
sshKeyPath: "/fake/keypath/id_rsa",
16+
}}
17+
18+
path, err := exec.LookPath("sshfs")
19+
if err != nil {
20+
t.Skip("sshfs not found (install sshfs ?)")
21+
}
22+
cmd, err := getMountCmd("myfunhost:/home/docker/foo", "/tmp/foo", false, &hostInfoLoader)
23+
24+
expectedArgs := append(
25+
baseSSHFSArgs,
26+
"-o",
27+
"IdentitiesOnly=yes",
28+
"-o",
29+
"Port=234",
30+
"-o",
31+
"IdentityFile=/fake/keypath/id_rsa",
32+
"[email protected]:/home/docker/foo",
33+
"/tmp/foo",
34+
)
35+
expectedCmd := exec.Command(path, expectedArgs...)
36+
37+
assert.Equal(t, expectedCmd, cmd)
38+
assert.NoError(t, err)
39+
}
40+
41+
func TestGetMountCmdWithoutSshKey(t *testing.T) {
42+
hostInfoLoader := MockHostInfoLoader{MockHostInfo{
43+
ip: "1.2.3.4",
44+
sshUsername: "user",
45+
}}
46+
47+
path, err := exec.LookPath("sshfs")
48+
if err != nil {
49+
t.Skip("sshfs not found (install sshfs ?)")
50+
}
51+
cmd, err := getMountCmd("myfunhost:/home/docker/foo", "", false, &hostInfoLoader)
52+
53+
expectedArgs := append(
54+
baseSSHFSArgs,
55+
"[email protected]:/home/docker/foo",
56+
"/home/docker/foo",
57+
)
58+
expectedCmd := exec.Command(path, expectedArgs...)
59+
60+
assert.Equal(t, expectedCmd, cmd)
61+
assert.NoError(t, err)
62+
}
63+
64+
func TestGetMountCmdUnmount(t *testing.T) {
65+
hostInfoLoader := MockHostInfoLoader{MockHostInfo{
66+
ip: "1.2.3.4",
67+
sshUsername: "user",
68+
}}
69+
70+
path, err := exec.LookPath("fusermount")
71+
if err != nil {
72+
t.Skip("fusermount not found (install fuse ?)")
73+
}
74+
cmd, err := getMountCmd("myfunhost:/home/docker/foo", "/tmp/foo", true, &hostInfoLoader)
75+
76+
expectedArgs := []string{
77+
"-u",
78+
"/tmp/foo",
79+
}
80+
expectedCmd := exec.Command(path, expectedArgs...)
81+
82+
assert.Equal(t, expectedCmd, cmd)
83+
assert.NoError(t, err)
84+
}

0 commit comments

Comments
 (0)