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