Files
docker-machine/libmachine/ssh/client.go
Nathan LeClaire b5927f10c4 Make libmachine usable by outside world
- Clear out some cruft tightly coupling libmachine to filestore

- Comment out drivers other than virtualbox for now

- Change way too many things

- Mostly, break out the code to be more modular.

- Destroy all traces of "provider" in its current form.  It will be
brought back as something more sensible, instead of something which
overlaps in function with both Host and Store.

- Fix mis-managed config passthru

- Remove a few instances of state stored in env vars

- This should be explicitly communicated in Go-land, not through the
shell.

- Rename "store" module to "persist"

- This is done mostly to avoid confusion about the fact that a concrete
instance of a "Store" interface is oftentimes referred to as "store" in
the code.

- Rip out repetitive antipattern for getting store

- This replaces the previous repetive idiom for getting the cert info, and
consequently the store, with a much less repetitive idiom.

- Also, some redundant methods in commands.go for accessing hosts have
either been simplified or removed entirely.

- First steps towards fixing up tests

- Test progress continues

- Replace unit tests with integration tests

- MAKE ALL UNIT TESTS PASS YAY

- Add helper test files

- Don't write to disk in libmachine/host

- Heh.. coverage check strikes again

- Fix remove code

- Move cert code around

- Continued progress: simplify Driver

- Fixups and make creation work with new model

- Move drivers module inside of libmachine

- Move ssh module inside of libmachine

- Move state module to libmachine

- Move utils module to libmachine

- Move version module to libmachine

- Move log module to libmachine

- Modify some constructor methods around

- Change Travis build dep structure

- Boring gofmt fix

- Add version module

- Move NewHost to store

- Update some boring cert path infos to make API easier to use

- Fix up some issues around the new model

- Clean up some cert path stuff

- Don't use shady functions to get store path :D

- Continue artifact work

- Fix silly machines dir bug

- Continue fixing silly path issues

- Change up output of vbm a bit

- Continue work to make example go

- Change output a little more

- Last changes needed to make create finish properly

- Fix config.go to use libmachine

- Cut down code duplication and make both methods work with libmachine

- Add pluggable logging implementation

- Return error when machine already in desired state

- Update example to show log method

- Fix file:// bug

- Fix Swarm defaults

- Remove unused TLS settings from Engine and Swarm options

- Remove spurious error

- Correct bug detecting if migration was performed

- Fix compilation errors from tests

- Fix most of remaining test issues

- Fix final silly bug in tests

- Remove extraneous debug code

- Add -race to test command

- Appease the gofmt

- Appease the generate coverage

- Making executive decision to remove Travis coverage check

In the early days I thought this would be a good idea because it would
encourage people to write tests in case they added a new module.  Well,
in fact it has just turned into a giant nuisance and made refactoring
work like this even more difficult.

- Move Get to Load
- Move HostListItem code to CLI

Signed-off-by: Nathan LeClaire <nathan.leclaire@gmail.com>
2015-09-23 12:30:15 -07:00

300 lines
6.6 KiB
Go

package ssh
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"github.com/docker/docker/pkg/term"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnutils"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
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
)
const (
External SSHClientType = "external"
Native SSHClientType = "native"
)
var (
baseSSHArgs = []string{
"-o", "PasswordAuthentication=no",
"-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
"-o", "ControlMaster=no", // disable ssh multiplexing
"-o", "ControlPath=no",
}
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 {
log.Debug("SSH binary not found, using native Go implementation")
return NewNativeClient(user, host, port, auth)
}
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 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 ssh.ClientConfig{}, err
}
privateKey, err := ssh.ParsePrivateKey(key)
if err != nil {
return ssh.ClientConfig{}, err
}
authMethods = append(authMethods, ssh.PublicKeys(privateKey))
}
for _, p := range auth.Passwords {
authMethods = append(authMethods, ssh.Password(p))
}
return ssh.ClientConfig{
User: user,
Auth: authMethods,
}, nil
}
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 NativeClient) session(command string) (*ssh.Session, error) {
if err := mcnutils.WaitFor(client.dialSuccess); err != nil {
return nil, fmt.Errorf("Error attempting SSH client dial: %s", err)
}
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config)
if err != nil {
return nil, fmt.Errorf("Mysterious error dialing TCP for SSH (we already succeeded at least once) : %s", err)
}
return conn.NewSession()
}
func (client NativeClient) Output(command string) (string, error) {
session, err := client.session(command)
if err != nil {
return "", nil
}
output, err := session.CombinedOutput(command)
defer session.Close()
return string(output), err
}
func (client NativeClient) OutputWithPty(command string) (string, error) {
session, err := client.session(command)
if err != nil {
return "", nil
}
fd := int(os.Stdin.Fd())
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
return "", err
}
modes := ssh.TerminalModes{
ssh.ECHO: 0,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
// request tty -- fixes error with hosts that use
// "Defaults requiretty" in /etc/sudoers - I'm looking at you RedHat
if err := session.RequestPty("xterm", termHeight, termWidth, modes); err != nil {
return "", err
}
output, err := session.CombinedOutput(command)
defer session.Close()
return string(output), err
}
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
}
session, err := conn.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
modes := ssh.TerminalModes{
ssh.ECHO: 1,
}
fd := os.Stdin.Fd()
if term.IsTerminal(fd) {
oldState, err := term.MakeRaw(fd)
if err != nil {
return err
}
defer term.RestoreTerminal(fd, oldState)
winsize, err := term.GetWinsize(fd)
if err != nil {
termWidth = 80
termHeight = 24
} else {
termWidth = int(winsize.Width)
termHeight = int(winsize.Height)
}
}
if err := session.RequestPty("xterm", termHeight, termWidth, modes); err != nil {
return err
}
if err := session.Shell(); err != nil {
return err
}
session.Wait()
return nil
}
func NewExternalClient(sshBinaryPath, user, host string, port int, auth *Auth) (ExternalClient, error) {
client := ExternalClient{
BinaryPath: sshBinaryPath,
}
args := append(baseSSHArgs, fmt.Sprintf("%s@%s", user, host))
// 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))
client.BaseArgs = args
return client, nil
}
func (client ExternalClient) Output(command string) (string, error) {
// TODO: Ugh, gross hack. Replace with all instances using variadic
// syntax
args := append(client.BaseArgs, strings.Split(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()
}