Move code to use SSH "backends"

Default to shelling out to SSH when available.

Signed-off-by: Nathan LeClaire <nathan.leclaire@gmail.com>
This commit is contained in:
Nathan LeClaire 2015-05-14 15:02:23 -07:00
parent 15e022219f
commit 2f78b7f92a
15 changed files with 309 additions and 165 deletions

View File

@ -1,29 +1,24 @@
package commands package commands
import ( import (
"io" "fmt"
"os"
"strings" "strings"
"github.com/docker/machine/log" "github.com/docker/machine/log"
"github.com/docker/machine/state"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/docker/machine/drivers"
"github.com/docker/machine/ssh"
) )
func cmdSsh(c *cli.Context) { func cmdSsh(c *cli.Context) {
var ( args := c.Args()
output ssh.Output name := args.First()
err error cmd := ""
)
if len(c.Args()) == 0 { if name == "" {
log.Fatal("Error: Please specify a machine name.") log.Fatal("Error: Please specify a machine name.")
} }
name := c.Args().First()
certInfo := getCertPathInfo(c) certInfo := getCertPathInfo(c)
defaultStore, err := getDefaultStore( defaultStore, err := getDefaultStore(
c.GlobalString("storage-path"), c.GlobalString("storage-path"),
@ -44,40 +39,50 @@ func cmdSsh(c *cli.Context) {
log.Fatal(err) log.Fatal(err)
} }
_, err = host.GetURL() currentState, err := host.Driver.GetState()
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)
}
if err != nil { if err != nil {
log.Fatal(err) 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)
}
} }

View File

@ -913,6 +913,30 @@ cgroup 499.8M 0 499.8M 0% /sys/fs/cgroup
/mnt/sda1/var/lib/docker/aufs /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 #### scp
Copy files from your local host to a machine, from machine to machine, or from a Copy files from your local host to a machine, from machine to machine, or from a

View File

@ -256,7 +256,7 @@ func (c *ComputeUtil) executeCommands(commands []string, ip, sshKeyPath string)
return err return err
} }
if _, err := client.Run(command); err != nil { if _, err := client.Output(command); err != nil {
return err return err
} }
} }

View File

@ -8,17 +8,15 @@ import (
"github.com/docker/machine/utils" "github.com/docker/machine/utils"
) )
func RunSSHCommandFromDriver(d Driver, command string) (ssh.Output, error) { func RunSSHCommandFromDriver(d Driver, command string) (string, error) {
var output ssh.Output
addr, err := d.GetSSHHostname() addr, err := d.GetSSHHostname()
if err != nil { if err != nil {
return output, err return "", err
} }
port, err := d.GetSSHPort() port, err := d.GetSSHPort()
if err != nil { if err != nil {
return output, err return "", err
} }
auth := &ssh.Auth{ 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) client, err := ssh.NewClient(d.GetSSHUsername(), addr, port, auth)
if err != nil { if err != nil {
return output, err return "", err
} }
log.Debugf("About to run SSH command:\n%s", command) 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) log.Debugf("SSH cmd err, output: %v: %s", err, output)
return output, err return output, err
} }

View File

@ -503,20 +503,15 @@ func (d *Driver) GetIP() (string, error) {
if s != state.Running { if s != state.Running {
return "", drivers.ErrHostIsNotRunning return "", drivers.ErrHostIsNotRunning
} }
output, err := drivers.RunSSHCommandFromDriver(d, "ip addr show dev eth1") output, err := drivers.RunSSHCommandFromDriver(d, "ip addr show dev eth1")
if err != nil { if err != nil {
return "", err return "", err
} }
var buf bytes.Buffer log.Debugf("SSH returned: %s\nEND SSH\n", output)
if _, err := buf.ReadFrom(output.Stdout); err != nil {
return "", err
}
out := buf.String()
log.Debugf("SSH returned: %s\nEND SSH\n", out)
// parse to find: inet 192.168.59.103/24 brd 192.168.59.255 scope global eth1 // 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 { for _, line := range lines {
vals := strings.Split(strings.TrimSpace(line), " ") vals := strings.Split(strings.TrimSpace(line), " ")
if len(vals) >= 2 && vals[0] == "inet" { 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 { func (d *Driver) publicSSHKeyPath() string {

View File

@ -2,10 +2,18 @@ package libmachine
import ( import (
"errors" "errors"
"fmt"
) )
var ( var (
ErrHostDoesNotExist = errors.New("Host does not exist")
ErrInvalidHostname = errors.New("Invalid hostname specified") ErrInvalidHostname = errors.New("Invalid hostname specified")
ErrUnknownProviderType = errors.New("Unknown hypervisor type") 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)
}

View File

@ -25,7 +25,9 @@ func NewFilestore(rootPath string, caCert string, privateKey string) *Filestore
func (s Filestore) loadHost(name string) (*Host, error) { func (s Filestore) loadHost(name string) (*Host, error) {
hostPath := filepath.Join(utils.GetMachineDir(), name) hostPath := filepath.Join(utils.GetMachineDir(), name)
if _, err := os.Stat(hostPath); os.IsNotExist(err) { if _, err := os.Stat(hostPath); os.IsNotExist(err) {
return nil, ErrHostDoesNotExist return nil, ErrHostDoesNotExist{
Name: name,
}
} }
host := &Host{Name: name, StorePath: hostPath} host := &Host{Name: name, StorePath: hostPath}

View File

@ -137,26 +137,30 @@ func (h *Host) Create(name string) error {
return nil 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) return drivers.RunSSHCommandFromDriver(h.Driver, command)
} }
func (h *Host) CreateSSHShell() error { func (h *Host) CreateSSHClient() (ssh.Client, error) {
addr, err := h.Driver.GetSSHHostname() addr, err := h.Driver.GetSSHHostname()
if err != nil { if err != nil {
return err return ssh.ExternalClient{}, err
} }
port, err := h.Driver.GetSSHPort() port, err := h.Driver.GetSSHPort()
if err != nil { if err != nil {
return err return ssh.ExternalClient{}, err
} }
auth := &ssh.Auth{ auth := &ssh.Auth{
Keys: []string{h.Driver.GetSSHKeyPath()}, 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 { if err != nil {
return err return err
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/docker/machine/libmachine/provision/pkgaction" "github.com/docker/machine/libmachine/provision/pkgaction"
"github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/libmachine/swarm"
"github.com/docker/machine/log" "github.com/docker/machine/log"
"github.com/docker/machine/ssh"
"github.com/docker/machine/state" "github.com/docker/machine/state"
"github.com/docker/machine/utils" "github.com/docker/machine/utils"
) )
@ -102,17 +101,7 @@ func (provisioner *Boot2DockerProvisioner) Package(name string, action pkgaction
} }
func (provisioner *Boot2DockerProvisioner) Hostname() (string, error) { func (provisioner *Boot2DockerProvisioner) Hostname() (string, error) {
output, err := provisioner.SSHCommand(fmt.Sprintf("hostname")) return 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
} }
func (provisioner *Boot2DockerProvisioner) SetHostname(hostname string) error { func (provisioner *Boot2DockerProvisioner) SetHostname(hostname string) error {
@ -231,7 +220,7 @@ func (provisioner *Boot2DockerProvisioner) Provision(swarmOptions swarm.SwarmOpt
return nil 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) return drivers.RunSSHCommandFromDriver(provisioner.Driver, args)
} }

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/machine/libmachine/auth" "github.com/docker/machine/libmachine/auth"
"github.com/docker/machine/libmachine/engine" "github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/libmachine/swarm"
"github.com/docker/machine/ssh"
) )
type GenericProvisioner struct { type GenericProvisioner struct {
@ -25,17 +24,7 @@ type GenericProvisioner struct {
} }
func (provisioner *GenericProvisioner) Hostname() (string, error) { func (provisioner *GenericProvisioner) Hostname() (string, error) {
output, err := provisioner.SSHCommand("hostname") return 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
} }
func (provisioner *GenericProvisioner) SetHostname(hostname string) error { func (provisioner *GenericProvisioner) SetHostname(hostname string) error {
@ -63,7 +52,7 @@ func (provisioner *GenericProvisioner) GetDockerOptionsDir() string {
return provisioner.DockerOptionsDir 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) return drivers.RunSSHCommandFromDriver(provisioner.Driver, args)
} }

View File

@ -1,7 +1,6 @@
package provision package provision
import ( import (
"bytes"
"fmt" "fmt"
"github.com/docker/machine/drivers" "github.com/docker/machine/drivers"
@ -9,7 +8,6 @@ import (
"github.com/docker/machine/libmachine/engine" "github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/provision/pkgaction" "github.com/docker/machine/libmachine/provision/pkgaction"
"github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/libmachine/swarm"
"github.com/docker/machine/ssh"
) )
var provisioners = make(map[string]*RegisteredProvisioner) var provisioners = make(map[string]*RegisteredProvisioner)
@ -52,7 +50,7 @@ type Provisioner interface {
GetDriver() drivers.Driver GetDriver() drivers.Driver
// Short-hand for accessing an SSH command from the 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 // Set the OS Release info depending on how it's represented
// internally // internally
@ -69,19 +67,12 @@ func Register(name string, p *RegisteredProvisioner) {
} }
func DetectProvisioner(d drivers.Driver) (Provisioner, error) { func DetectProvisioner(d drivers.Driver) (Provisioner, error) {
var ( osReleaseOut, err := drivers.RunSSHCommandFromDriver(d, "cat /etc/os-release")
osReleaseOut bytes.Buffer
)
catOsReleaseOutput, err := drivers.RunSSHCommandFromDriver(d, "cat /etc/os-release")
if err != nil { if err != nil {
return nil, fmt.Errorf("Error getting SSH command: %s", err) return nil, fmt.Errorf("Error getting SSH command: %s", err)
} }
if _, err := osReleaseOut.ReadFrom(catOsReleaseOutput.Stdout); err != nil { osReleaseInfo, err := NewOsRelease([]byte(osReleaseOut))
return nil, err
}
osReleaseInfo, err := NewOsRelease(osReleaseOut.Bytes())
if err != nil { if err != nil {
return nil, fmt.Errorf("Error parsing /etc/os-release file: %s", err) return nil, fmt.Errorf("Error parsing /etc/os-release file: %s", err)
} }

View File

@ -1,7 +1,6 @@
package provision package provision
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
@ -26,12 +25,7 @@ func installDockerGeneric(p Provisioner) error {
// install docker - until cloudinit we use ubuntu everywhere so we // install docker - until cloudinit we use ubuntu everywhere so we
// just install it using the docker repos // 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 { if output, err := p.SSHCommand("if ! type docker; then curl -sSL https://get.docker.com | sh -; fi"); err != nil {
var buf bytes.Buffer return fmt.Errorf("error installing docker: %s\n", output)
if _, err := buf.ReadFrom(output.Stderr); err != nil {
return err
}
return fmt.Errorf("error installing docker: %s\n", buf.String())
} }
return nil return nil

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/machine/commands" "github.com/docker/machine/commands"
"github.com/docker/machine/log" "github.com/docker/machine/log"
"github.com/docker/machine/ssh"
"github.com/docker/machine/utils" "github.com/docker/machine/utils"
"github.com/docker/machine/version" "github.com/docker/machine/version"
) )
@ -58,6 +59,9 @@ func main() {
app.Email = "https://github.com/docker/machine" app.Email = "https://github.com/docker/machine"
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {
os.Setenv("MACHINE_STORAGE_PATH", c.GlobalString("storage-path")) os.Setenv("MACHINE_STORAGE_PATH", c.GlobalString("storage-path"))
if c.GlobalBool("native-ssh") {
ssh.SetDefaultClient(ssh.Native)
}
return nil return nil
} }
app.Commands = commands.Commands app.Commands = commands.Commands
@ -100,6 +104,11 @@ func main() {
Usage: "Private key used in client TLS auth", Usage: "Private key used in client TLS auth",
Value: "", Value: "",
}, },
cli.BoolFlag{
EnvVar: "MACHINE_NATIVE_SSH",
Name: "native-ssh",
Usage: "Use the native (Go-based) SSH implementation.",
},
} }
app.Run(os.Args) app.Run(os.Args)

View File

@ -1,11 +1,10 @@
package ssh package ssh
import ( import (
"bytes"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
"github.com/docker/machine/log" "github.com/docker/machine/log"
@ -13,41 +12,108 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type Client struct { type Client interface {
Config *ssh.ClientConfig Output(command string) (string, error)
Shell() error
}
type ExternalClient struct {
BaseArgs []string
BinaryPath string
}
type NativeClient struct {
Config ssh.ClientConfig
Hostname string Hostname string
Port int Port int
} }
type Auth struct {
Passwords []string
Keys []string
}
type SSHClientType string
const ( const (
maxDialAttempts = 10 maxDialAttempts = 10
) )
func NewClient(user string, host string, port int, auth *Auth) (*Client, error) { const (
config, err := NewConfig(user, auth) 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 { 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, Config: config,
Hostname: host, Hostname: host,
Port: port, Port: port,
}, nil }, nil
} }
func NewConfig(user string, auth *Auth) (*ssh.ClientConfig, error) { func NewNativeConfig(user string, auth *Auth) (ssh.ClientConfig, error) {
var authMethods []ssh.AuthMethod var (
authMethods []ssh.AuthMethod
)
for _, k := range auth.Keys { for _, k := range auth.Keys {
key, err := ioutil.ReadFile(k) key, err := ioutil.ReadFile(k)
if err != nil { if err != nil {
return nil, err return ssh.ClientConfig{}, err
} }
privateKey, err := ssh.ParsePrivateKey(key) privateKey, err := ssh.ParsePrivateKey(key)
if err != nil { if err != nil {
return nil, err return ssh.ClientConfig{}, err
} }
authMethods = append(authMethods, ssh.PublicKeys(privateKey)) 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)) authMethods = append(authMethods, ssh.Password(p))
} }
return &ssh.ClientConfig{ return ssh.ClientConfig{
User: user, User: user,
Auth: authMethods, Auth: authMethods,
}, nil }, nil
} }
func dialSuccess(client *Client) func() bool { func (client NativeClient) dialSuccess() bool {
return func() bool { if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config); err != nil {
if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), client.Config); err != nil {
log.Debugf("Error dialing TCP: %s", err) log.Debugf("Error dialing TCP: %s", err)
return false return false
} }
return true return true
}
} }
func (client *Client) Run(command string) (Output, error) { func (client NativeClient) Output(command string) (string, error) {
var ( if err := utils.WaitFor(client.dialSuccess); err != nil {
output Output return "", fmt.Errorf("Error attempting SSH client dial: %s", err)
stdout, stderr bytes.Buffer
)
if err := utils.WaitFor(dialSuccess(client)); err != nil {
return output, 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 { 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() session, err := conn.NewSession()
if err != nil { 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() defer session.Close()
session.Stdout = &stdout output, err := session.CombinedOutput(command)
session.Stderr = &stderr
output = Output{ return string(output), err
Stdout: &stdout,
Stderr: &stderr,
}
return output, session.Run(command)
} }
func (client *Client) Shell() error { func (client NativeClient) Shell() error {
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), client.Config) var (
termWidth, termHeight int
)
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config)
if err != nil { if err != nil {
return err return err
} }
@ -127,14 +183,10 @@ func (client *Client) Shell() error {
ssh.ECHO: 1, ssh.ECHO: 1,
} }
var termWidth, termHeight int
fd := os.Stdin.Fd() fd := os.Stdin.Fd()
if term.IsTerminal(fd) { if term.IsTerminal(fd) {
var oldState *term.State oldState, err := term.MakeRaw(fd)
oldState, err = term.MakeRaw(fd)
if err != nil { if err != nil {
return err return err
} }
@ -164,12 +216,51 @@ func (client *Client) Shell() error {
return nil return nil
} }
type Auth struct { func NewExternalClient(sshBinaryPath, user, host string, port int, auth *Auth) (ExternalClient, error) {
Passwords []string client := ExternalClient{
Keys []string 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 { func (client ExternalClient) Output(command string) (string, error) {
Stdout io.Reader args := append(client.BaseArgs, command)
Stderr io.Reader
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()
} }

View File

@ -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 ]
}