packer-cn/helper/communicator/step_connect_winrm.go
Chris Roberts 8bb7d8dc16 Prevent busy loop while waiting for WinRM connection
Set first run variable outside conditional so it will properly be
updated during the first run and pause on subsequent runs.
2019-10-09 17:55:16 -07:00

187 lines
5.0 KiB
Go

package communicator
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"strings"
"time"
"github.com/hashicorp/packer/communicator/winrm"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
winrmcmd "github.com/masterzen/winrm"
)
// StepConnectWinRM is a multistep Step implementation that waits for WinRM
// to become available. It gets the connection information from a single
// configuration when creating the step.
//
// Uses:
// ui packer.Ui
//
// Produces:
// communicator packer.Communicator
type StepConnectWinRM struct {
// All the fields below are documented on StepConnect
Config *Config
Host func(multistep.StateBag) (string, error)
WinRMConfig func(multistep.StateBag) (*WinRMConfig, error)
WinRMPort func(multistep.StateBag) (int, error)
}
func (s *StepConnectWinRM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
var comm packer.Communicator
var err error
subCtx, cancel := context.WithCancel(ctx)
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for WinRM to become available...")
comm, err = s.waitForWinRM(state, subCtx)
cancel() // just to make 'possible context leak' analysis happy
waitDone <- true
}()
log.Printf("Waiting for WinRM, up to timeout: %s", s.Config.WinRMTimeout)
timeout := time.After(s.Config.WinRMTimeout)
for {
// Wait for either WinRM to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for WinRM: %s", err))
return multistep.ActionHalt
}
ui.Say("Connected to WinRM!")
state.Put("communicator", comm)
return multistep.ActionContinue
case <-timeout:
err := fmt.Errorf("Timeout waiting for WinRM.")
state.Put("error", err)
ui.Error(err.Error())
cancel()
return multistep.ActionHalt
case <-ctx.Done():
// The step sequence was cancelled, so cancel waiting for WinRM
// and just start the halting process.
cancel()
log.Println("Interrupt detected, quitting waiting for WinRM.")
return multistep.ActionHalt
case <-time.After(1 * time.Second):
}
}
}
func (s *StepConnectWinRM) Cleanup(multistep.StateBag) {
}
func (s *StepConnectWinRM) waitForWinRM(state multistep.StateBag, ctx context.Context) (packer.Communicator, error) {
var comm packer.Communicator
first := true
for {
// Don't check for cancel or wait on first iteration
if !first {
select {
case <-ctx.Done():
log.Println("[INFO] WinRM wait cancelled. Exiting loop.")
return nil, errors.New("WinRM wait cancelled")
case <-time.After(5 * time.Second):
}
}
first = false
host, err := s.Host(state)
if err != nil {
log.Printf("[DEBUG] Error getting WinRM host: %s", err)
continue
}
port := s.Config.WinRMPort
if s.WinRMPort != nil {
port, err = s.WinRMPort(state)
if err != nil {
log.Printf("[DEBUG] Error getting WinRM port: %s", err)
continue
}
}
user := s.Config.WinRMUser
password := s.Config.WinRMPassword
if s.WinRMConfig != nil {
config, err := s.WinRMConfig(state)
if err != nil {
log.Printf("[DEBUG] Error getting WinRM config: %s", err)
continue
}
if config.Username != "" {
user = config.Username
}
if config.Password != "" {
password = config.Password
}
}
log.Println("[INFO] Attempting WinRM connection...")
comm, err = winrm.New(&winrm.Config{
Host: host,
Port: port,
Username: user,
Password: password,
Timeout: s.Config.WinRMTimeout,
Https: s.Config.WinRMUseSSL,
Insecure: s.Config.WinRMInsecure,
TransportDecorator: s.Config.WinRMTransportDecorator,
})
if err != nil {
log.Printf("[ERROR] WinRM connection err: %s", err)
continue
}
break
}
// run an "echo" command to make sure winrm is actually connected before moving on.
var connectCheckCommand = winrmcmd.Powershell(`if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'}; echo "WinRM connected."`)
var retryableSleep = 5 * time.Second
// run an "echo" command to make sure that the winrm is connected
for {
cmd := &packer.RemoteCmd{Command: connectCheckCommand}
var buf, buf2 bytes.Buffer
cmd.Stdout = &buf
cmd.Stdout = io.MultiWriter(cmd.Stdout, &buf2)
select {
case <-ctx.Done():
log.Println("WinRM wait canceled, exiting loop")
return comm, fmt.Errorf("WinRM wait canceled")
case <-time.After(retryableSleep):
}
log.Printf("Checking that WinRM is connected with: '%s'", connectCheckCommand)
ui := state.Get("ui").(packer.Ui)
err := cmd.RunWithUi(ctx, comm, ui)
if err != nil {
log.Printf("Communication connection err: %s", err)
continue
}
log.Printf("Connected to machine")
stdoutToRead := buf2.String()
if !strings.Contains(stdoutToRead, "WinRM connected.") {
log.Printf("echo didn't succeed; retrying...")
continue
}
break
}
return comm, nil
}