github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/remote/ssh.go (about)

     1  package remote
     2  
     3  import (
     4  	"io"
     5  	"time"
     6  
     7  	"github.com/mongodb/grip"
     8  	"github.com/pkg/errors"
     9  	"golang.org/x/crypto/ssh"
    10  )
    11  
    12  var (
    13  	ErrCmdTimedOut = errors.New("ssh command timed out")
    14  )
    15  
    16  // SSHCommand abstracts a single command to be run via ssh, on a remote machine.
    17  type SSHCommand struct {
    18  	// the command to be run on the remote machine
    19  	Command string
    20  
    21  	// the remote host to connect to
    22  	Host string
    23  
    24  	// the user to connect with
    25  	User string
    26  
    27  	// the location of the private key file (PEM-encoded) to be used
    28  	Keyfile string
    29  
    30  	// stdin for the remote command
    31  	Stdin io.Reader
    32  
    33  	// the threshold at which the command is considered to time out, and will be killed
    34  	Timeout time.Duration
    35  }
    36  
    37  // Run the command via ssh. Returns the combined stdout and stderr, as well as any
    38  // error that occurs.
    39  func (cmd *SSHCommand) Run() ([]byte, error) {
    40  
    41  	// configure appropriately
    42  	clientConfig, err := createClientConfig(cmd.User, cmd.Keyfile)
    43  	if err != nil {
    44  		return nil, errors.Wrap(err, "error configuring ssh")
    45  	}
    46  
    47  	// open a connection to the ssh server
    48  	conn, err := ssh.Dial("tcp", cmd.Host, clientConfig)
    49  	if err != nil {
    50  		return nil, errors.Wrapf(err, "error connecting to ssh server at `%v`", cmd.Host)
    51  	}
    52  
    53  	// initiate a session for running an ssh command
    54  	session, err := conn.NewSession()
    55  	if err != nil {
    56  		return nil, errors.Wrapf(err, "error creating an ssh session to `%v`", cmd.Host)
    57  	}
    58  	defer session.Close()
    59  
    60  	// set stdin appropriately
    61  	session.Stdin = cmd.Stdin
    62  
    63  	// terminal modes for the pty we'll be using
    64  	modes := ssh.TerminalModes{
    65  		ssh.ECHO:          0,     // disable echoing
    66  		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
    67  		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    68  	}
    69  
    70  	// request a pseudo terminal
    71  	if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
    72  		return nil, errors.Errorf("error requesting pty: %v", err)
    73  	}
    74  
    75  	// kick the ssh command off
    76  	errChan := make(chan error)
    77  	output := []byte{}
    78  	go func() {
    79  		output, err = session.CombinedOutput(cmd.Command)
    80  		errChan <- errors.WithStack(err)
    81  	}()
    82  
    83  	// wait for the command to finish, or time out
    84  	select {
    85  	case err := <-errChan:
    86  		return output, errors.WithStack(err)
    87  	case <-time.After(cmd.Timeout):
    88  		// command timed out; kill the remote process
    89  		grip.CatchError(errors.WithStack(session.Signal(ssh.SIGKILL)))
    90  		return nil, ErrCmdTimedOut
    91  	}
    92  
    93  }