launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  // TODO(axw) fallback to go.crypto/ssh if no native client is available.
     9  package ssh
    10  
    11  import (
    12  	"bytes"
    13  	"io"
    14  	"launchpad.net/errgo/errors"
    15  	"os/exec"
    16  	"syscall"
    17  
    18  	"launchpad.net/juju-core/cmd"
    19  )
    20  
    21  var mask = errors.Mask
    22  
    23  // Options is a client-implementation independent SSH options set.
    24  type Options struct {
    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  // SetPort sets the SSH server port to connect to.
    38  func (o *Options) SetPort(port int) {
    39  	o.port = port
    40  }
    41  
    42  // EnablePTY forces the allocation of a pseudo-TTY.
    43  //
    44  // Forcing a pseudo-TTY is required, for example, for sudo
    45  // prompts on the target host.
    46  func (o *Options) EnablePTY() {
    47  	o.allocatePTY = true
    48  }
    49  
    50  // AllowPasswordAuthentication allows the SSH
    51  // client to prompt the user for a password.
    52  //
    53  // Password authentication is disallowed by default.
    54  func (o *Options) AllowPasswordAuthentication() {
    55  	o.passwordAuthAllowed = true
    56  }
    57  
    58  // SetIdentities sets a sequence of paths to private key/identity files
    59  // to use when attempting login. Client implementations may attempt to
    60  // use additional identities, but must give preference to the ones
    61  // specified here.
    62  func (o *Options) SetIdentities(identityFiles ...string) {
    63  	o.identities = append([]string{}, identityFiles...)
    64  }
    65  
    66  // Client is an interface for SSH clients to implement
    67  type Client interface {
    68  	// Command returns a Command for executing a command
    69  	// on the specified host. Each Command is executed
    70  	// within its own SSH session.
    71  	//
    72  	// Host is specified in the format [user@]host.
    73  	Command(host string, command []string, options *Options) *Cmd
    74  
    75  	// Copy copies a file between the local host and
    76  	// target host. Paths are specified in the scp format,
    77  	// [[user@]host:]path.
    78  	Copy(source, dest string, options *Options) error
    79  }
    80  
    81  // Cmd represents a command to be (or being) executed
    82  // on a remote host.
    83  type Cmd struct {
    84  	Stdin  io.Reader
    85  	Stdout io.Writer
    86  	Stderr io.Writer
    87  	impl   command
    88  }
    89  
    90  // CombinedOutput runs the command, and returns the
    91  // combined stdout/stderr output and result of
    92  // executing the command.
    93  func (c *Cmd) CombinedOutput() ([]byte, error) {
    94  	if c.Stdout != nil {
    95  		return nil, errors.New("ssh: Stdout already set")
    96  	}
    97  	if c.Stderr != nil {
    98  		return nil, errors.New("ssh: Stderr already set")
    99  	}
   100  	var b bytes.Buffer
   101  	c.Stdout = &b
   102  	c.Stderr = &b
   103  	err := c.Run()
   104  	return b.Bytes(), err
   105  }
   106  
   107  // Output runs the command, and returns the stdout
   108  // output and result of executing the command.
   109  func (c *Cmd) Output() ([]byte, error) {
   110  	if c.Stdout != nil {
   111  		return nil, errors.New("ssh: Stdout already set")
   112  	}
   113  	var b bytes.Buffer
   114  	c.Stdout = &b
   115  	err := c.Run()
   116  	return b.Bytes(), err
   117  }
   118  
   119  // Run runs the command, and returns the result as an error.
   120  func (c *Cmd) Run() error {
   121  	if err := c.Start(); err != nil {
   122  		return mask(err)
   123  	}
   124  	err := c.Wait()
   125  	if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
   126  		status := exitError.ProcessState.Sys().(syscall.WaitStatus)
   127  		if status.Exited() {
   128  			return cmd.NewRcPassthroughError(status.ExitStatus())
   129  		}
   130  	}
   131  	return err
   132  }
   133  
   134  // Start starts the command running, but does not wait for
   135  // it to complete. If the command could not be started, an
   136  // error is returned.
   137  func (c *Cmd) Start() error {
   138  	c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr)
   139  	return c.impl.Start()
   140  }
   141  
   142  // Wait waits for the started command to complete,
   143  // and returns the result as an error.
   144  func (c *Cmd) Wait() error {
   145  	return c.impl.Wait()
   146  }
   147  
   148  // Kill kills the started command.
   149  func (c *Cmd) Kill() error {
   150  	return c.impl.Kill()
   151  }
   152  
   153  // StdinPipe creates a pipe and connects it to
   154  // the command's stdin. The read end of the pipe
   155  // is assigned to c.Stdin.
   156  func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
   157  	wc, r, err := c.impl.StdinPipe()
   158  	if err != nil {
   159  		return nil, mask(err)
   160  	}
   161  	c.Stdin = r
   162  	return wc, nil
   163  }
   164  
   165  // StdoutPipe creates a pipe and connects it to
   166  // the command's stdout. The write end of the pipe
   167  // is assigned to c.Stdout.
   168  func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
   169  	rc, w, err := c.impl.StdoutPipe()
   170  	if err != nil {
   171  		return nil, mask(err)
   172  	}
   173  	c.Stdout = w
   174  	return rc, nil
   175  }
   176  
   177  // StderrPipe creates a pipe and connects it to
   178  // the command's stderr. The write end of the pipe
   179  // is assigned to c.Stderr.
   180  func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
   181  	rc, w, err := c.impl.StderrPipe()
   182  	if err != nil {
   183  		return nil, mask(err)
   184  	}
   185  	c.Stderr = w
   186  	return rc, nil
   187  }
   188  
   189  // command is an implementation-specific representation of a
   190  // command prepared to execute against a specific host.
   191  type command interface {
   192  	Start() error
   193  	Wait() error
   194  	Kill() error
   195  	SetStdio(stdin io.Reader, stdout, stderr io.Writer)
   196  	StdinPipe() (io.WriteCloser, io.Reader, error)
   197  	StdoutPipe() (io.ReadCloser, io.Writer, error)
   198  	StderrPipe() (io.ReadCloser, io.Writer, error)
   199  }
   200  
   201  // DefaultClient is the default SSH client for the process.
   202  //
   203  // If the OpenSSH client is found in $PATH, then it will be
   204  // used for DefaultClient; otherwise, DefaultClient will use
   205  // an embedded client based on go.crypto/ssh.
   206  var DefaultClient Client
   207  
   208  func init() {
   209  	initDefaultClient()
   210  }
   211  
   212  func initDefaultClient() {
   213  	if client, err := NewOpenSSHClient(); err == nil {
   214  		DefaultClient = client
   215  	} else if client, err := NewGoCryptoClient(); err == nil {
   216  		DefaultClient = client
   217  	}
   218  }
   219  
   220  // Command is a short-cut for DefaultClient.Command.
   221  func Command(host string, command []string, options *Options) *Cmd {
   222  	return DefaultClient.Command(host, command, options)
   223  }
   224  
   225  // Copy is a short-cut for DefaultClient.Copy.
   226  func Copy(source, dest string, options *Options) error {
   227  	return DefaultClient.Copy(source, dest, options)
   228  }