diff --git a/commands/ssh.go b/commands/ssh.go index 6e77a63a76..56cafd7557 100644 --- a/commands/ssh.go +++ b/commands/ssh.go @@ -1,29 +1,24 @@ package commands import ( - "io" - "os" + "fmt" "strings" "github.com/docker/machine/log" + "github.com/docker/machine/state" "github.com/codegangsta/cli" - "github.com/docker/machine/drivers" - "github.com/docker/machine/ssh" ) func cmdSsh(c *cli.Context) { - var ( - output ssh.Output - err error - ) + args := c.Args() + name := args.First() + cmd := "" - if len(c.Args()) == 0 { + if name == "" { log.Fatal("Error: Please specify a machine name.") } - name := c.Args().First() - certInfo := getCertPathInfo(c) defaultStore, err := getDefaultStore( c.GlobalString("storage-path"), @@ -44,40 +39,50 @@ func cmdSsh(c *cli.Context) { log.Fatal(err) } - _, err = host.GetURL() - if err != nil { - if err == drivers.ErrHostIsNotRunning { - log.Fatalf("%s is not running. Please start this with docker-machine start %s", host.Name, host.Name) - } else { - log.Fatalf("Unexpected error getting machine url: %s", err) - } - } - - if len(c.Args()) == 1 { - err = host.CreateSSHShell() - } else { - var ( - cmd string - args []string = c.Args() - ) - - for i, arg := range args { - if arg == "--" { - i++ - cmd = strings.Join(args[i:], " ") - break - } - } - if len(cmd) == 0 { - cmd = strings.Join(args[1:], " ") - } - output, err = host.RunSSHCommand(cmd) - - io.Copy(os.Stderr, output.Stderr) - io.Copy(os.Stdout, output.Stdout) - } - + currentState, err := host.Driver.GetState() if err != nil { log.Fatal(err) } + + if currentState != state.Running { + log.Fatalf("Error: Cannot run SSH command: Host %q is not running", host.Name) + } + + // Loop through the arguments and parse out a command which relies on + // flags if it exists, for instance an invocation of the form + // `docker-machine ssh dev -- df -h` would mandate this, otherwise we + // will accidentally trigger the codegangsta/cli help text because it + // thinks we are trying to specify codegangsta flags. + // + // TODO: I thought codegangsta/cli supported the flag parsing + // terminator manually, which would mitigate the need for this kind of + // hack. We should investigate. + for i, arg := range args { + if arg == "--" { + cmd = strings.Join(args[i+1:], " ") + break + } + } + + // It is possible that the user has specified an appended command which + // does not rely on the flag parsing terminator, such as + // `docker-machine ssh dev ls`, so this block accounts for that case. + if len(cmd) == 0 { + cmd = strings.Join(args[1:], " ") + } + + if len(c.Args()) == 1 { + err := host.CreateSSHShell() + if err != nil { + log.Fatal(err) + } + } else { + output, err := host.RunSSHCommand(cmd) + if err != nil { + log.Fatal(err) + } + + fmt.Print(output) + } + } diff --git a/docs/index.md b/docs/index.md index 25aea2045c..82e0c2561d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -913,6 +913,30 @@ cgroup 499.8M 0 499.8M 0% /sys/fs/cgroup /mnt/sda1/var/lib/docker/aufs ``` +##### Different types of SSH + +When Docker Machine is invoked, it will check to see if you have the venerable +`ssh` binary around locally and will attempt to use that for the SSH commands it +needs to run, whether they are a part of an operation such as creation or have +been requested by the user directly. If it does not find an external `ssh` +binary locally, it will default to using a native Go implementation from +[crypto/ssh](https://godoc.org/golang.org/x/crypto/ssh). This is useful in +situations where you may not have access to traditional UNIX tools, such as if +you are using Docker Machine on Windows without having msysgit installed +alongside of it. + +In most situations, you will not have to worry about this implementation detail +and Docker Machine will act sensibly out of the box. However, if you +deliberately want to use the Go native version, you can do so with a global +command line flag / environment variable like so: + +``` +$ docker-machine --native-ssh ssh dev +``` + +There are some variations in behavior between the two methods, so please report +any issues or inconsistencies if you come across them. + #### scp Copy files from your local host to a machine, from machine to machine, or from a diff --git a/drivers/google/compute_util.go b/drivers/google/compute_util.go index 015dc8ed65..bdf26672d7 100644 --- a/drivers/google/compute_util.go +++ b/drivers/google/compute_util.go @@ -256,7 +256,7 @@ func (c *ComputeUtil) executeCommands(commands []string, ip, sshKeyPath string) return err } - if _, err := client.Run(command); err != nil { + if _, err := client.Output(command); err != nil { return err } } diff --git a/drivers/utils.go b/drivers/utils.go index a59127ec27..4b221816d4 100644 --- a/drivers/utils.go +++ b/drivers/utils.go @@ -8,17 +8,15 @@ import ( "github.com/docker/machine/utils" ) -func RunSSHCommandFromDriver(d Driver, command string) (ssh.Output, error) { - var output ssh.Output - +func RunSSHCommandFromDriver(d Driver, command string) (string, error) { addr, err := d.GetSSHHostname() if err != nil { - return output, err + return "", err } port, err := d.GetSSHPort() if err != nil { - return output, err + return "", err } auth := &ssh.Auth{ @@ -27,11 +25,11 @@ func RunSSHCommandFromDriver(d Driver, command string) (ssh.Output, error) { client, err := ssh.NewClient(d.GetSSHUsername(), addr, port, auth) if err != nil { - return output, err + return "", err } log.Debugf("About to run SSH command:\n%s", command) - output, err = client.Run(command) + output, err := client.Output(command) log.Debugf("SSH cmd err, output: %v: %s", err, output) return output, err } diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go index 4b0f97d954..a61f365085 100644 --- a/drivers/virtualbox/virtualbox.go +++ b/drivers/virtualbox/virtualbox.go @@ -503,20 +503,15 @@ func (d *Driver) GetIP() (string, error) { if s != state.Running { return "", drivers.ErrHostIsNotRunning } + output, err := drivers.RunSSHCommandFromDriver(d, "ip addr show dev eth1") if err != nil { return "", err } - var buf bytes.Buffer - if _, err := buf.ReadFrom(output.Stdout); err != nil { - return "", err - } - - out := buf.String() - log.Debugf("SSH returned: %s\nEND SSH\n", out) + log.Debugf("SSH returned: %s\nEND SSH\n", output) // parse to find: inet 192.168.59.103/24 brd 192.168.59.255 scope global eth1 - lines := strings.Split(out, "\n") + lines := strings.Split(output, "\n") for _, line := range lines { vals := strings.Split(strings.TrimSpace(line), " ") if len(vals) >= 2 && vals[0] == "inet" { @@ -524,7 +519,7 @@ func (d *Driver) GetIP() (string, error) { } } - return "", fmt.Errorf("No IP address found %s", out) + return "", fmt.Errorf("No IP address found %s", output) } func (d *Driver) publicSSHKeyPath() string { diff --git a/libmachine/errors.go b/libmachine/errors.go index a3119c897c..e973dc2597 100644 --- a/libmachine/errors.go +++ b/libmachine/errors.go @@ -2,10 +2,18 @@ package libmachine import ( "errors" + "fmt" ) var ( - ErrHostDoesNotExist = errors.New("Host does not exist") ErrInvalidHostname = errors.New("Invalid hostname specified") ErrUnknownProviderType = errors.New("Unknown hypervisor type") ) + +type ErrHostDoesNotExist struct { + Name string +} + +func (e ErrHostDoesNotExist) Error() string { + return fmt.Sprintf("Error: Host does not exist: %s", e.Name) +} diff --git a/libmachine/filestore.go b/libmachine/filestore.go index 18db82a50c..7cdebb6394 100644 --- a/libmachine/filestore.go +++ b/libmachine/filestore.go @@ -25,7 +25,9 @@ func NewFilestore(rootPath string, caCert string, privateKey string) *Filestore func (s Filestore) loadHost(name string) (*Host, error) { hostPath := filepath.Join(utils.GetMachineDir(), name) if _, err := os.Stat(hostPath); os.IsNotExist(err) { - return nil, ErrHostDoesNotExist + return nil, ErrHostDoesNotExist{ + Name: name, + } } host := &Host{Name: name, StorePath: hostPath} diff --git a/libmachine/host.go b/libmachine/host.go index 6508a5f9a9..53cf221b9e 100644 --- a/libmachine/host.go +++ b/libmachine/host.go @@ -137,26 +137,30 @@ func (h *Host) Create(name string) error { return nil } -func (h *Host) RunSSHCommand(command string) (ssh.Output, error) { +func (h *Host) RunSSHCommand(command string) (string, error) { return drivers.RunSSHCommandFromDriver(h.Driver, command) } -func (h *Host) CreateSSHShell() error { +func (h *Host) CreateSSHClient() (ssh.Client, error) { addr, err := h.Driver.GetSSHHostname() if err != nil { - return err + return ssh.ExternalClient{}, err } port, err := h.Driver.GetSSHPort() if err != nil { - return err + return ssh.ExternalClient{}, err } auth := &ssh.Auth{ Keys: []string{h.Driver.GetSSHKeyPath()}, } - client, err := ssh.NewClient(h.Driver.GetSSHUsername(), addr, port, auth) + return ssh.NewClient(h.Driver.GetSSHUsername(), addr, port, auth) +} + +func (h *Host) CreateSSHShell() error { + client, err := h.CreateSSHClient() if err != nil { return err } diff --git a/libmachine/provision/boot2docker.go b/libmachine/provision/boot2docker.go index 33e9952f8c..c8619c458c 100644 --- a/libmachine/provision/boot2docker.go +++ b/libmachine/provision/boot2docker.go @@ -13,7 +13,6 @@ import ( "github.com/docker/machine/libmachine/provision/pkgaction" "github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/log" - "github.com/docker/machine/ssh" "github.com/docker/machine/state" "github.com/docker/machine/utils" ) @@ -102,17 +101,7 @@ func (provisioner *Boot2DockerProvisioner) Package(name string, action pkgaction } func (provisioner *Boot2DockerProvisioner) Hostname() (string, error) { - output, err := provisioner.SSHCommand(fmt.Sprintf("hostname")) - if err != nil { - return "", err - } - - var so bytes.Buffer - if _, err := so.ReadFrom(output.Stdout); err != nil { - return "", err - } - - return so.String(), nil + return provisioner.SSHCommand("hostname") } func (provisioner *Boot2DockerProvisioner) SetHostname(hostname string) error { @@ -231,7 +220,7 @@ func (provisioner *Boot2DockerProvisioner) Provision(swarmOptions swarm.SwarmOpt return nil } -func (provisioner *Boot2DockerProvisioner) SSHCommand(args string) (ssh.Output, error) { +func (provisioner *Boot2DockerProvisioner) SSHCommand(args string) (string, error) { return drivers.RunSSHCommandFromDriver(provisioner.Driver, args) } diff --git a/libmachine/provision/generic.go b/libmachine/provision/generic.go index 5ec1d99aa5..a2991e4b80 100644 --- a/libmachine/provision/generic.go +++ b/libmachine/provision/generic.go @@ -9,7 +9,6 @@ import ( "github.com/docker/machine/libmachine/auth" "github.com/docker/machine/libmachine/engine" "github.com/docker/machine/libmachine/swarm" - "github.com/docker/machine/ssh" ) type GenericProvisioner struct { @@ -25,17 +24,7 @@ type GenericProvisioner struct { } func (provisioner *GenericProvisioner) Hostname() (string, error) { - output, err := provisioner.SSHCommand("hostname") - if err != nil { - return "", err - } - - var so bytes.Buffer - if _, err := so.ReadFrom(output.Stdout); err != nil { - return "", err - } - - return so.String(), nil + return provisioner.SSHCommand("hostname") } func (provisioner *GenericProvisioner) SetHostname(hostname string) error { @@ -63,7 +52,7 @@ func (provisioner *GenericProvisioner) GetDockerOptionsDir() string { return provisioner.DockerOptionsDir } -func (provisioner *GenericProvisioner) SSHCommand(args string) (ssh.Output, error) { +func (provisioner *GenericProvisioner) SSHCommand(args string) (string, error) { return drivers.RunSSHCommandFromDriver(provisioner.Driver, args) } diff --git a/libmachine/provision/provisioner.go b/libmachine/provision/provisioner.go index 8eed87b101..2a9df31112 100644 --- a/libmachine/provision/provisioner.go +++ b/libmachine/provision/provisioner.go @@ -1,7 +1,6 @@ package provision import ( - "bytes" "fmt" "github.com/docker/machine/drivers" @@ -9,7 +8,6 @@ import ( "github.com/docker/machine/libmachine/engine" "github.com/docker/machine/libmachine/provision/pkgaction" "github.com/docker/machine/libmachine/swarm" - "github.com/docker/machine/ssh" ) var provisioners = make(map[string]*RegisteredProvisioner) @@ -52,7 +50,7 @@ type Provisioner interface { GetDriver() drivers.Driver // Short-hand for accessing an SSH command from the driver. - SSHCommand(args string) (ssh.Output, error) + SSHCommand(args string) (string, error) // Set the OS Release info depending on how it's represented // internally @@ -69,19 +67,12 @@ func Register(name string, p *RegisteredProvisioner) { } func DetectProvisioner(d drivers.Driver) (Provisioner, error) { - var ( - osReleaseOut bytes.Buffer - ) - catOsReleaseOutput, err := drivers.RunSSHCommandFromDriver(d, "cat /etc/os-release") + osReleaseOut, err := drivers.RunSSHCommandFromDriver(d, "cat /etc/os-release") if err != nil { return nil, fmt.Errorf("Error getting SSH command: %s", err) } - if _, err := osReleaseOut.ReadFrom(catOsReleaseOutput.Stdout); err != nil { - return nil, err - } - - osReleaseInfo, err := NewOsRelease(osReleaseOut.Bytes()) + osReleaseInfo, err := NewOsRelease([]byte(osReleaseOut)) if err != nil { return nil, fmt.Errorf("Error parsing /etc/os-release file: %s", err) } diff --git a/libmachine/provision/utils.go b/libmachine/provision/utils.go index 8b368111bd..27c720d358 100644 --- a/libmachine/provision/utils.go +++ b/libmachine/provision/utils.go @@ -1,7 +1,6 @@ package provision import ( - "bytes" "fmt" "io/ioutil" "net/url" @@ -26,12 +25,7 @@ func installDockerGeneric(p Provisioner) error { // install docker - until cloudinit we use ubuntu everywhere so we // just install it using the docker repos if output, err := p.SSHCommand("if ! type docker; then curl -sSL https://get.docker.com | sh -; fi"); err != nil { - var buf bytes.Buffer - if _, err := buf.ReadFrom(output.Stderr); err != nil { - return err - } - - return fmt.Errorf("error installing docker: %s\n", buf.String()) + return fmt.Errorf("error installing docker: %s\n", output) } return nil diff --git a/main.go b/main.go index 2fb7e8c260..a37bf1d908 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/docker/machine/commands" "github.com/docker/machine/log" + "github.com/docker/machine/ssh" "github.com/docker/machine/utils" "github.com/docker/machine/version" ) @@ -58,6 +59,9 @@ func main() { app.Email = "https://github.com/docker/machine" app.Before = func(c *cli.Context) error { os.Setenv("MACHINE_STORAGE_PATH", c.GlobalString("storage-path")) + if c.GlobalBool("native-ssh") { + ssh.SetDefaultClient(ssh.Native) + } return nil } app.Commands = commands.Commands @@ -100,6 +104,11 @@ func main() { Usage: "Private key used in client TLS auth", Value: "", }, + cli.BoolFlag{ + EnvVar: "MACHINE_NATIVE_SSH", + Name: "native-ssh", + Usage: "Use the native (Go-based) SSH implementation.", + }, } app.Run(os.Args) diff --git a/ssh/client.go b/ssh/client.go index cf5b2cfe3c..a102fa5ca6 100644 --- a/ssh/client.go +++ b/ssh/client.go @@ -1,11 +1,10 @@ package ssh import ( - "bytes" "fmt" - "io" "io/ioutil" "os" + "os/exec" "github.com/docker/docker/pkg/term" "github.com/docker/machine/log" @@ -13,41 +12,108 @@ import ( "golang.org/x/crypto/ssh" ) -type Client struct { - Config *ssh.ClientConfig +type Client interface { + Output(command string) (string, error) + Shell() error +} + +type ExternalClient struct { + BaseArgs []string + BinaryPath string +} + +type NativeClient struct { + Config ssh.ClientConfig Hostname string Port int } +type Auth struct { + Passwords []string + Keys []string +} + +type SSHClientType string + const ( maxDialAttempts = 10 ) -func NewClient(user string, host string, port int, auth *Auth) (*Client, error) { - config, err := NewConfig(user, auth) +const ( + External SSHClientType = "external" + Native SSHClientType = "native" +) + +var ( + baseSSHArgs = []string{ + "-o", "IdentitiesOnly=yes", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=quiet", // suppress "Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts." + "-o", "ConnectionAttempts=3", // retry 3 times if SSH connection fails + "-o", "ConnectTimeout=10", // timeout after 10 seconds + } + defaultClientType SSHClientType = External +) + +func SetDefaultClient(clientType SSHClientType) { + // Allow over-riding of default client type, so that even if ssh binary + // is found in PATH we can still use the Go native implementation if + // desired. + switch clientType { + case External: + defaultClientType = External + case Native: + defaultClientType = Native + } +} + +func NewClient(user string, host string, port int, auth *Auth) (Client, error) { + sshBinaryPath, err := exec.LookPath("ssh") if err != nil { - return nil, err + if defaultClientType == External { + log.Fatal("Requested shellout SSH client type but no ssh binary available") + } + log.Debug("ssh binary not found, using native Go implementation") + return NewNativeClient(user, host, port, auth) } - return &Client{ + if defaultClientType == Native { + log.Debug("Using SSH client type: native") + return NewNativeClient(user, host, port, auth) + } + + log.Debug("Using SSH client type: external") + return NewExternalClient(sshBinaryPath, user, host, port, auth) +} + +func NewNativeClient(user, host string, port int, auth *Auth) (Client, error) { + config, err := NewNativeConfig(user, auth) + if err != nil { + return nil, fmt.Errorf("Error getting config for native Go SSH: %s", err) + } + + return NativeClient{ Config: config, Hostname: host, Port: port, }, nil } -func NewConfig(user string, auth *Auth) (*ssh.ClientConfig, error) { - var authMethods []ssh.AuthMethod +func NewNativeConfig(user string, auth *Auth) (ssh.ClientConfig, error) { + var ( + authMethods []ssh.AuthMethod + ) for _, k := range auth.Keys { key, err := ioutil.ReadFile(k) if err != nil { - return nil, err + return ssh.ClientConfig{}, err } privateKey, err := ssh.ParsePrivateKey(key) if err != nil { - return nil, err + return ssh.ClientConfig{}, err } authMethods = append(authMethods, ssh.PublicKeys(privateKey)) @@ -57,57 +123,47 @@ func NewConfig(user string, auth *Auth) (*ssh.ClientConfig, error) { authMethods = append(authMethods, ssh.Password(p)) } - return &ssh.ClientConfig{ + return ssh.ClientConfig{ User: user, Auth: authMethods, }, nil } -func dialSuccess(client *Client) func() bool { - return func() bool { - if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), client.Config); err != nil { - log.Debugf("Error dialing TCP: %s", err) - return false - } - return true +func (client NativeClient) dialSuccess() bool { + if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config); err != nil { + log.Debugf("Error dialing TCP: %s", err) + return false } + return true } -func (client *Client) Run(command string) (Output, error) { - var ( - output Output - stdout, stderr bytes.Buffer - ) - - if err := utils.WaitFor(dialSuccess(client)); err != nil { - return output, fmt.Errorf("Error attempting SSH client dial: %s", err) +func (client NativeClient) Output(command string) (string, error) { + if err := utils.WaitFor(client.dialSuccess); err != nil { + return "", fmt.Errorf("Error attempting SSH client dial: %s", err) } - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), client.Config) + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config) if err != nil { - return output, fmt.Errorf("Mysterious error dialing TCP for SSH (we already succeeded at least once) : %s", err) + return "", fmt.Errorf("Mysterious error dialing TCP for SSH (we already succeeded at least once) : %s", err) } session, err := conn.NewSession() if err != nil { - return output, fmt.Errorf("Error getting new session: %s", err) + return "", fmt.Errorf("Error getting new session: %s", err) } defer session.Close() - session.Stdout = &stdout - session.Stderr = &stderr + output, err := session.CombinedOutput(command) - output = Output{ - Stdout: &stdout, - Stderr: &stderr, - } - - return output, session.Run(command) + return string(output), err } -func (client *Client) Shell() error { - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), client.Config) +func (client NativeClient) Shell() error { + var ( + termWidth, termHeight int + ) + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config) if err != nil { return err } @@ -127,14 +183,10 @@ func (client *Client) Shell() error { ssh.ECHO: 1, } - var termWidth, termHeight int - fd := os.Stdin.Fd() if term.IsTerminal(fd) { - var oldState *term.State - - oldState, err = term.MakeRaw(fd) + oldState, err := term.MakeRaw(fd) if err != nil { return err } @@ -164,12 +216,51 @@ func (client *Client) Shell() error { return nil } -type Auth struct { - Passwords []string - Keys []string +func NewExternalClient(sshBinaryPath, user, host string, port int, auth *Auth) (ExternalClient, error) { + client := ExternalClient{ + BinaryPath: sshBinaryPath, + } + + // Base args take care of settings some options for us, e.g. don't use + // the authorized hosts file. + args := baseSSHArgs + + // Specify which private keys to use to authorize the SSH request. + for _, privateKeyPath := range auth.Keys { + args = append(args, "-i", privateKeyPath) + } + + // Set which port to use for SSH. + args = append(args, "-p", fmt.Sprintf("%d", port)) + + // Set the user and hostname, e.g. ubuntu@12.34.56.78 + args = append(args, fmt.Sprintf("%s@%s", user, host)) + + client.BaseArgs = args + + return client, nil } -type Output struct { - Stdout io.Reader - Stderr io.Reader +func (client ExternalClient) Output(command string) (string, error) { + args := append(client.BaseArgs, command) + + cmd := exec.Command(client.BinaryPath, args...) + log.Debug(cmd) + + // Allow piping of local things to remote commands. + cmd.Stdin = os.Stdin + + output, err := cmd.CombinedOutput() + return string(output), err +} + +func (client ExternalClient) Shell() error { + cmd := exec.Command(client.BinaryPath, client.BaseArgs...) + log.Debug(cmd) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() } diff --git a/test/integration/ssh-backends.bats b/test/integration/ssh-backends.bats new file mode 100644 index 0000000000..9bf0ad8ca4 --- /dev/null +++ b/test/integration/ssh-backends.bats @@ -0,0 +1,45 @@ +#!/usr/bin/env bats + +load helpers + +export DRIVER=virtualbox +export NAME="bats-$DRIVER-test" +export MACHINE_STORAGE_PATH=/tmp/machine-bats-test-$DRIVER + +# Basic smoke test for SSH backends + +@test "$DRIVER: create SSH test box" { + run machine create -d $DRIVER $NAME + [[ "$status" -eq 0 ]] +} + +@test "$DRIVER: test external ssh backend" { + run machine ssh $NAME -- df -h + [[ "$status" -eq 0 ]] +} + +@test "$DRIVER: test command did what it purported to -- external ssh" { + run machine ssh $NAME echo foo + [[ "$output" == "foo" ]] +} + +@test "$DRIVER: test native ssh backend" { + run machine --native-ssh ssh $NAME -- df -h + [[ "$status" -eq 0 ]] +} + +@test "$DRIVER: test command did what it purported to -- native ssh" { + run machine --native-ssh ssh $NAME echo foo + [[ "$output" == "foo" ]] +} + +@test "$DRIVER: remove machine after ssh backend test" { + run machine rm -f $NAME +} + +# Cleanup of machine store should always be the last 'test' +@test "$DRIVER: cleanup" { + run rm -rf $MACHINE_STORAGE_PATH + [ "$status" -eq 0 ] +} +