240 lines
7.0 KiB
Go
240 lines
7.0 KiB
Go
package communicator
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
|
|
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/sdk-internals/communicator/winrm"
|
|
winrmcmd "github.com/masterzen/winrm"
|
|
"golang.org/x/net/http/httpproxy"
|
|
)
|
|
|
|
// 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 packersdk.Ui
|
|
//
|
|
// Produces:
|
|
// communicator packersdk.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").(packersdk.Ui)
|
|
|
|
var comm packersdk.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) (packersdk.Communicator, error) {
|
|
var comm packersdk.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
|
|
}
|
|
s.Config.WinRMHost = host
|
|
|
|
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
|
|
}
|
|
s.Config.WinRMPort = port
|
|
}
|
|
|
|
state.Put("communicator_config", s.Config)
|
|
|
|
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
|
|
s.Config.WinRMPassword = password
|
|
}
|
|
}
|
|
|
|
if s.Config.WinRMNoProxy {
|
|
if err := setNoProxy(host, port); err != nil {
|
|
return nil, fmt.Errorf("Error setting no_proxy: %s", err)
|
|
}
|
|
s.Config.WinRMTransportDecorator = ProxyTransportDecorator
|
|
}
|
|
|
|
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 := &packersdk.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").(packersdk.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
|
|
}
|
|
|
|
// setNoProxy configures the $NO_PROXY env var
|
|
func setNoProxy(host string, port int) error {
|
|
current := os.Getenv("NO_PROXY")
|
|
p := fmt.Sprintf("%s:%d", host, port)
|
|
if current == "" {
|
|
return os.Setenv("NO_PROXY", p)
|
|
}
|
|
if !strings.Contains(current, p) {
|
|
return os.Setenv("NO_PROXY", strings.Join([]string{current, p}, ","))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The net/http ProxyFromEnvironment only loads the environment once, when the
|
|
// code is initialized rather than when it's executed. This means that if your
|
|
// wrapping code sets the NO_PROXY env var (as Packer does!), it will be
|
|
// ignored. Re-loading the environment vars is more expensive, but it is the
|
|
// easiest way to work around this limitation.
|
|
func RefreshProxyFromEnvironment(req *http.Request) (*url.URL, error) {
|
|
return envProxyFunc()(req.URL)
|
|
}
|
|
|
|
func envProxyFunc() func(*url.URL) (*url.URL, error) {
|
|
envProxyFuncValue := httpproxy.FromEnvironment().ProxyFunc()
|
|
return envProxyFuncValue
|
|
}
|
|
|
|
// ProxyTransportDecorator is a custom Transporter that reloads HTTP Proxy settings at client runtime.
|
|
// The net/http ProxyFromEnvironment only loads the environment once, when the
|
|
// code is initialized rather than when it's executed. This means that if your
|
|
// wrapping code sets the NO_PROXY env var (as Packer does!), it will be
|
|
// ignored. Re-loading the environment vars is more expensive, but it is the
|
|
// easiest way to work around this limitation.
|
|
func ProxyTransportDecorator() winrmcmd.Transporter {
|
|
return winrmcmd.NewClientWithProxyFunc(RefreshProxyFromEnvironment)
|
|
}
|