github.com/phobos182/packer@v0.2.3-0.20130819023704-c84d2aeffc68/common/step_connect_ssh.go (about)

     1  package common
     2  
     3  import (
     4  	gossh "code.google.com/p/go.crypto/ssh"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/mitchellh/multistep"
     8  	"github.com/mitchellh/packer/communicator/ssh"
     9  	"github.com/mitchellh/packer/packer"
    10  	"log"
    11  	"strings"
    12  	"time"
    13  )
    14  
    15  // StepConnectSSH is a multistep Step implementation that waits for SSH
    16  // to become available. It gets the connection information from a single
    17  // configuration when creating the step.
    18  //
    19  // Uses:
    20  //   ui packer.Ui
    21  //
    22  // Produces:
    23  //   communicator packer.Communicator
    24  type StepConnectSSH struct {
    25  	// SSHAddress is a function that returns the TCP address to connect to
    26  	// for SSH. This is a function so that you can query information
    27  	// if necessary for this address.
    28  	SSHAddress func(map[string]interface{}) (string, error)
    29  
    30  	// SSHConfig is a function that returns the proper client configuration
    31  	// for SSH access.
    32  	SSHConfig func(map[string]interface{}) (*gossh.ClientConfig, error)
    33  
    34  	// SSHWaitTimeout is the total timeout to wait for SSH to become available.
    35  	SSHWaitTimeout time.Duration
    36  
    37  	comm packer.Communicator
    38  }
    39  
    40  func (s *StepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
    41  	ui := state["ui"].(packer.Ui)
    42  
    43  	var comm packer.Communicator
    44  	var err error
    45  
    46  	cancel := make(chan struct{})
    47  	waitDone := make(chan bool, 1)
    48  	go func() {
    49  		ui.Say("Waiting for SSH to become available...")
    50  		comm, err = s.waitForSSH(state, cancel)
    51  		waitDone <- true
    52  	}()
    53  
    54  	log.Printf("Waiting for SSH, up to timeout: %s", s.SSHWaitTimeout)
    55  	timeout := time.After(s.SSHWaitTimeout)
    56  WaitLoop:
    57  	for {
    58  		// Wait for either SSH to become available, a timeout to occur,
    59  		// or an interrupt to come through.
    60  		select {
    61  		case <-waitDone:
    62  			if err != nil {
    63  				ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
    64  				return multistep.ActionHalt
    65  			}
    66  
    67  			ui.Say("Connected to SSH!")
    68  			s.comm = comm
    69  			state["communicator"] = comm
    70  			break WaitLoop
    71  		case <-timeout:
    72  			ui.Error("Timeout waiting for SSH.")
    73  			close(cancel)
    74  			return multistep.ActionHalt
    75  		case <-time.After(1 * time.Second):
    76  			if _, ok := state[multistep.StateCancelled]; ok {
    77  				// The step sequence was cancelled, so cancel waiting for SSH
    78  				// and just start the halting process.
    79  				close(cancel)
    80  				log.Println("Interrupt detected, quitting waiting for SSH.")
    81  				return multistep.ActionHalt
    82  			}
    83  		}
    84  	}
    85  
    86  	return multistep.ActionContinue
    87  }
    88  
    89  func (s *StepConnectSSH) Cleanup(map[string]interface{}) {
    90  }
    91  
    92  func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan struct{}) (packer.Communicator, error) {
    93  	handshakeAttempts := 0
    94  
    95  	var comm packer.Communicator
    96  	for {
    97  		select {
    98  		case <-cancel:
    99  			log.Println("SSH wait cancelled. Exiting loop.")
   100  			return nil, errors.New("SSH wait cancelled")
   101  		case <-time.After(5 * time.Second):
   102  		}
   103  
   104  		// First we request the TCP connection information
   105  		address, err := s.SSHAddress(state)
   106  		if err != nil {
   107  			log.Printf("Error getting SSH address: %s", err)
   108  			continue
   109  		}
   110  
   111  		// Retrieve the SSH configuration
   112  		sshConfig, err := s.SSHConfig(state)
   113  		if err != nil {
   114  			log.Printf("Error getting SSH config: %s", err)
   115  			continue
   116  		}
   117  
   118  		// Attempt to connect to SSH port
   119  		connFunc := ssh.ConnectFunc("tcp", address, 5*time.Minute)
   120  		nc, err := connFunc()
   121  		if err != nil {
   122  			log.Printf("TCP connection to SSH ip/port failed: %s", err)
   123  			continue
   124  		}
   125  		nc.Close()
   126  
   127  		// Then we attempt to connect via SSH
   128  		config := &ssh.Config{
   129  			Connection: connFunc,
   130  			SSHConfig:  sshConfig,
   131  		}
   132  
   133  		log.Println("Attempting SSH connection...")
   134  		comm, err = ssh.New(config)
   135  		if err != nil {
   136  			log.Printf("SSH handshake err: %s", err)
   137  
   138  			// Only count this as an attempt if we were able to attempt
   139  			// to authenticate. Note this is very brittle since it depends
   140  			// on the string of the error... but I don't see any other way.
   141  			if strings.Contains(err.Error(), "authenticate") {
   142  				log.Printf("Detected authentication error. Increasing handshake attempts.")
   143  				handshakeAttempts += 1
   144  			}
   145  
   146  			if handshakeAttempts < 10 {
   147  				// Try to connect via SSH a handful of times
   148  				continue
   149  			}
   150  
   151  			return nil, err
   152  		}
   153  
   154  		break
   155  	}
   156  
   157  	return comm, nil
   158  }