github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/utils/ssh/ssh.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package ssh contains utilities for dealing with SSH connections,
     5  // key management, and so on. All SSH-based command executions in
     6  // Juju should use the Command/ScpCommand functions in this package.
     7  //
     8  package ssh
     9  
    10  import (
    11  	"bytes"
    12  	"errors"
    13  	"io"
    14  	"os/exec"
    15  	"syscall"
    16  
    17  	"github.com/juju/juju/cmd"
    18  )
    19  
    20  // Options is a client-implementation independent SSH options set.
    21  type Options struct {
    22  	// proxyCommand specifies the command to
    23  	// execute to proxy SSH traffic through.
    24  	proxyCommand []string
    25  	// ssh server port; zero means use the default (22)
    26  	port int
    27  	// no PTY forced by default
    28  	allocatePTY bool
    29  	// password authentication is disallowed by default
    30  	passwordAuthAllowed bool
    31  	// identities is a sequence of paths to private key/identity files
    32  	// to use when attempting to login. A client implementaton may attempt
    33  	// with additional identities, but must give preference to these
    34  	identities []string
    35  }
    36  
    37  // SetProxyCommand sets a command to execute to proxy traffic through.
    38  func (o *Options) SetProxyCommand(command ...string) {
    39  	o.proxyCommand = append([]string{}, command...)
    40  }
    41  
    42  // SetPort sets the SSH server port to connect to.
    43  func (o *Options) SetPort(port int) {
    44  	o.port = port
    45  }
    46  
    47  // EnablePTY forces the allocation of a pseudo-TTY.
    48  //
    49  // Forcing a pseudo-TTY is required, for example, for sudo
    50  // prompts on the target host.
    51  func (o *Options) EnablePTY() {
    52  	o.allocatePTY = true
    53  }
    54  
    55  // AllowPasswordAuthentication allows the SSH
    56  // client to prompt the user for a password.
    57  //
    58  // Password authentication is disallowed by default.
    59  func (o *Options) AllowPasswordAuthentication() {
    60  	o.passwordAuthAllowed = true
    61  }
    62  
    63  // SetIdentities sets a sequence of paths to private key/identity files
    64  // to use when attempting login. Client implementations may attempt to
    65  // use additional identities, but must give preference to the ones
    66  // specified here.
    67  func (o *Options) SetIdentities(identityFiles ...string) {
    68  	o.identities = append([]string{}, identityFiles...)
    69  }
    70  
    71  // Client is an interface for SSH clients to implement
    72  type Client interface {
    73  	// Command returns a Command for executing a command
    74  	// on the specified host. Each Command is executed
    75  	// within its own SSH session.
    76  	//
    77  	// Host is specified in the format [user@]host.
    78  	Command(host string, command []string, options *Options) *Cmd
    79  
    80  	// Copy copies file(s) between local and remote host(s).
    81  	// Paths are specified in the scp format, [[user@]host:]path. If
    82  	// any extra arguments are specified in extraArgs, they are passed
    83  	// verbatim.
    84  	Copy(args []string, options *Options) error
    85  }
    86  
    87  // Cmd represents a command to be (or being) executed
    88  // on a remote host.
    89  type Cmd struct {
    90  	Stdin  io.Reader
    91  	Stdout io.Writer
    92  	Stderr io.Writer
    93  	impl   command
    94  }
    95  
    96  // CombinedOutput runs the command, and returns the
    97  // combined stdout/stderr output and result of
    98  // executing the command.
    99  func (c *Cmd) CombinedOutput() ([]byte, error) {
   100  	if c.Stdout != nil {
   101  		return nil, errors.New("ssh: Stdout already set")
   102  	}
   103  	if c.Stderr != nil {
   104  		return nil, errors.New("ssh: Stderr already set")
   105  	}
   106  	var b bytes.Buffer
   107  	c.Stdout = &b
   108  	c.Stderr = &b
   109  	err := c.Run()
   110  	return b.Bytes(), err
   111  }
   112  
   113  // Output runs the command, and returns the stdout
   114  // output and result of executing the command.
   115  func (c *Cmd) Output() ([]byte, error) {
   116  	if c.Stdout != nil {
   117  		return nil, errors.New("ssh: Stdout already set")
   118  	}
   119  	var b bytes.Buffer
   120  	c.Stdout = &b
   121  	err := c.Run()
   122  	return b.Bytes(), err
   123  }
   124  
   125  // Run runs the command, and returns the result as an error.
   126  func (c *Cmd) Run() error {
   127  	if err := c.Start(); err != nil {
   128  		return err
   129  	}
   130  	err := c.Wait()
   131  	if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
   132  		status := exitError.ProcessState.Sys().(syscall.WaitStatus)
   133  		if status.Exited() {
   134  			return cmd.NewRcPassthroughError(status.ExitStatus())
   135  		}
   136  	}
   137  	return err
   138  }
   139  
   140  // Start starts the command running, but does not wait for
   141  // it to complete. If the command could not be started, an
   142  // error is returned.
   143  func (c *Cmd) Start() error {
   144  	c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr)
   145  	return c.impl.Start()
   146  }
   147  
   148  // Wait waits for the started command to complete,
   149  // and returns the result as an error.
   150  func (c *Cmd) Wait() error {
   151  	return c.impl.Wait()
   152  }
   153  
   154  // Kill kills the started command.
   155  func (c *Cmd) Kill() error {
   156  	return c.impl.Kill()
   157  }
   158  
   159  // StdinPipe creates a pipe and connects it to
   160  // the command's stdin. The read end of the pipe
   161  // is assigned to c.Stdin.
   162  func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
   163  	wc, r, err := c.impl.StdinPipe()
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	c.Stdin = r
   168  	return wc, nil
   169  }
   170  
   171  // StdoutPipe creates a pipe and connects it to
   172  // the command's stdout. The write end of the pipe
   173  // is assigned to c.Stdout.
   174  func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
   175  	rc, w, err := c.impl.StdoutPipe()
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	c.Stdout = w
   180  	return rc, nil
   181  }
   182  
   183  // StderrPipe creates a pipe and connects it to
   184  // the command's stderr. The write end of the pipe
   185  // is assigned to c.Stderr.
   186  func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
   187  	rc, w, err := c.impl.StderrPipe()
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	c.Stderr = w
   192  	return rc, nil
   193  }
   194  
   195  // command is an implementation-specific representation of a
   196  // command prepared to execute against a specific host.
   197  type command interface {
   198  	Start() error
   199  	Wait() error
   200  	Kill() error
   201  	SetStdio(stdin io.Reader, stdout, stderr io.Writer)
   202  	StdinPipe() (io.WriteCloser, io.Reader, error)
   203  	StdoutPipe() (io.ReadCloser, io.Writer, error)
   204  	StderrPipe() (io.ReadCloser, io.Writer, error)
   205  }
   206  
   207  // DefaultClient is the default SSH client for the process.
   208  //
   209  // If the OpenSSH client is found in $PATH, then it will be
   210  // used for DefaultClient; otherwise, DefaultClient will use
   211  // an embedded client based on go.crypto/ssh.
   212  var DefaultClient Client
   213  
   214  // chosenClient holds the type of SSH client created for
   215  // DefaultClient, so that we can log it in Command or Copy.
   216  var chosenClient string
   217  
   218  func init() {
   219  	initDefaultClient()
   220  }
   221  
   222  func initDefaultClient() {
   223  	if client, err := NewOpenSSHClient(); err == nil {
   224  		DefaultClient = client
   225  		chosenClient = "OpenSSH"
   226  	} else if client, err := NewGoCryptoClient(); err == nil {
   227  		DefaultClient = client
   228  		chosenClient = "go.crypto (embedded)"
   229  	}
   230  }
   231  
   232  // Command is a short-cut for DefaultClient.Command.
   233  func Command(host string, command []string, options *Options) *Cmd {
   234  	logger.Debugf("using %s ssh client", chosenClient)
   235  	return DefaultClient.Command(host, command, options)
   236  }
   237  
   238  // Copy is a short-cut for DefaultClient.Copy.
   239  func Copy(args []string, options *Options) error {
   240  	logger.Debugf("using %s ssh client", chosenClient)
   241  	return DefaultClient.Copy(args, options)
   242  }