github.com/ddnomad/packer@v1.3.2/provisioner/windows-restart/provisioner.go (about)

     1  package restart
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  
     8  	"log"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/hashicorp/packer/common"
    14  	"github.com/hashicorp/packer/helper/config"
    15  	"github.com/hashicorp/packer/packer"
    16  	"github.com/hashicorp/packer/template/interpolate"
    17  	"github.com/masterzen/winrm"
    18  )
    19  
    20  var DefaultRestartCommand = `shutdown /r /f /t 0 /c "packer restart"`
    21  var DefaultRestartCheckCommand = winrm.Powershell(`echo "${env:COMPUTERNAME} restarted."`)
    22  var retryableSleep = 5 * time.Second
    23  var TryCheckReboot = `shutdown /r /f /t 60 /c "packer restart test"`
    24  var AbortReboot = `shutdown /a`
    25  
    26  type Config struct {
    27  	common.PackerConfig `mapstructure:",squash"`
    28  
    29  	// The command used to restart the guest machine
    30  	RestartCommand string `mapstructure:"restart_command"`
    31  
    32  	// The command used to check if the guest machine has restarted
    33  	// The output of this command will be displayed to the user
    34  	RestartCheckCommand string `mapstructure:"restart_check_command"`
    35  
    36  	// The timeout for waiting for the machine to restart
    37  	RestartTimeout time.Duration `mapstructure:"restart_timeout"`
    38  
    39  	ctx interpolate.Context
    40  }
    41  
    42  type Provisioner struct {
    43  	config     Config
    44  	comm       packer.Communicator
    45  	ui         packer.Ui
    46  	cancel     chan struct{}
    47  	cancelLock sync.Mutex
    48  }
    49  
    50  func (p *Provisioner) Prepare(raws ...interface{}) error {
    51  	err := config.Decode(&p.config, &config.DecodeOpts{
    52  		Interpolate:        true,
    53  		InterpolateContext: &p.config.ctx,
    54  		InterpolateFilter: &interpolate.RenderFilter{
    55  			Exclude: []string{
    56  				"execute_command",
    57  			},
    58  		},
    59  	}, raws...)
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	if p.config.RestartCommand == "" {
    65  		p.config.RestartCommand = DefaultRestartCommand
    66  	}
    67  
    68  	if p.config.RestartCheckCommand == "" {
    69  		p.config.RestartCheckCommand = DefaultRestartCheckCommand
    70  	}
    71  
    72  	if p.config.RestartTimeout == 0 {
    73  		p.config.RestartTimeout = 5 * time.Minute
    74  	}
    75  
    76  	return nil
    77  }
    78  
    79  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
    80  	p.cancelLock.Lock()
    81  	p.cancel = make(chan struct{})
    82  	p.cancelLock.Unlock()
    83  
    84  	ui.Say("Restarting Machine")
    85  	p.comm = comm
    86  	p.ui = ui
    87  
    88  	var cmd *packer.RemoteCmd
    89  	command := p.config.RestartCommand
    90  	err := p.retryable(func() error {
    91  		cmd = &packer.RemoteCmd{Command: command}
    92  		return cmd.StartWithUi(comm, ui)
    93  	})
    94  
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	if cmd.ExitStatus != 0 {
   100  		return fmt.Errorf("Restart script exited with non-zero exit status: %d", cmd.ExitStatus)
   101  	}
   102  
   103  	return waitForRestart(p, comm)
   104  }
   105  
   106  var waitForRestart = func(p *Provisioner, comm packer.Communicator) error {
   107  	ui := p.ui
   108  	ui.Say("Waiting for machine to restart...")
   109  	waitDone := make(chan bool, 1)
   110  	timeout := time.After(p.config.RestartTimeout)
   111  	var err error
   112  
   113  	p.comm = comm
   114  	var cmd *packer.RemoteCmd
   115  	trycommand := TryCheckReboot
   116  	abortcommand := AbortReboot
   117  
   118  	// This sleep works around an azure/winrm bug. For more info see
   119  	// https://github.com/hashicorp/packer/issues/5257; we can remove the
   120  	// sleep when the underlying bug has been resolved.
   121  	time.Sleep(1 * time.Second)
   122  
   123  	// Stolen from Vagrant reboot checker
   124  	for {
   125  		log.Printf("Check if machine is rebooting...")
   126  		cmd = &packer.RemoteCmd{Command: trycommand}
   127  		err = cmd.StartWithUi(comm, ui)
   128  		if err != nil {
   129  			// Couldn't execute, we assume machine is rebooting already
   130  			break
   131  		}
   132  		if cmd.ExitStatus == 1 {
   133  			// SSH provisioner, and we're already rebooting. SSH can reconnect
   134  			// without our help; exit this wait loop.
   135  			break
   136  		}
   137  		if cmd.ExitStatus == 1115 || cmd.ExitStatus == 1190 || cmd.ExitStatus == 1717 {
   138  			// Reboot already in progress but not completed
   139  			log.Printf("Reboot already in progress, waiting...")
   140  			time.Sleep(10 * time.Second)
   141  		}
   142  		if cmd.ExitStatus == 0 {
   143  			// Cancel reboot we created to test if machine was already rebooting
   144  			cmd = &packer.RemoteCmd{Command: abortcommand}
   145  			cmd.StartWithUi(comm, ui)
   146  			break
   147  		}
   148  	}
   149  
   150  	go func() {
   151  		log.Printf("Waiting for machine to become available...")
   152  		err = waitForCommunicator(p)
   153  		waitDone <- true
   154  	}()
   155  
   156  	log.Printf("Waiting for machine to reboot with timeout: %s", p.config.RestartTimeout)
   157  
   158  WaitLoop:
   159  	for {
   160  		// Wait for either WinRM to become available, a timeout to occur,
   161  		// or an interrupt to come through.
   162  		select {
   163  		case <-waitDone:
   164  			if err != nil {
   165  				ui.Error(fmt.Sprintf("Error waiting for machine to restart: %s", err))
   166  				return err
   167  			}
   168  
   169  			ui.Say("Machine successfully restarted, moving on")
   170  			close(p.cancel)
   171  			break WaitLoop
   172  		case <-timeout:
   173  			err := fmt.Errorf("Timeout waiting for machine to restart.")
   174  			ui.Error(err.Error())
   175  			close(p.cancel)
   176  			return err
   177  		case <-p.cancel:
   178  			close(waitDone)
   179  			return fmt.Errorf("Interrupt detected, quitting waiting for machine to restart")
   180  		}
   181  	}
   182  	return nil
   183  
   184  }
   185  
   186  var waitForCommunicator = func(p *Provisioner) error {
   187  	runCustomRestartCheck := true
   188  	if p.config.RestartCheckCommand == DefaultRestartCheckCommand {
   189  		runCustomRestartCheck = false
   190  	}
   191  	// This command is configurable by the user to make sure that the
   192  	// vm has met their necessary criteria for having restarted. If the
   193  	// user doesn't set a special restart command, we just run the
   194  	// default as cmdModuleLoad below.
   195  	cmdRestartCheck := &packer.RemoteCmd{Command: p.config.RestartCheckCommand}
   196  	log.Printf("Checking that communicator is connected with: '%s'",
   197  		cmdRestartCheck.Command)
   198  	for {
   199  		select {
   200  		case <-p.cancel:
   201  			log.Println("Communicator wait canceled, exiting loop")
   202  			return fmt.Errorf("Communicator wait canceled")
   203  		case <-time.After(retryableSleep):
   204  		}
   205  		if runCustomRestartCheck {
   206  			// run user-configured restart check
   207  			err := cmdRestartCheck.StartWithUi(p.comm, p.ui)
   208  			if err != nil {
   209  				log.Printf("Communication connection err: %s", err)
   210  				continue
   211  			}
   212  			log.Printf("Connected to machine")
   213  			runCustomRestartCheck = false
   214  		}
   215  
   216  		// This is the non-user-configurable check that powershell
   217  		// modules have loaded.
   218  
   219  		// If we catch the restart in just the right place, we will be able
   220  		// to run the restart check but the output will be an error message
   221  		// about how it needs powershell modules to load, and we will start
   222  		// provisioning before powershell is actually ready.
   223  		// In this next check, we parse stdout to make sure that the command is
   224  		// actually running as expected.
   225  		cmdModuleLoad := &packer.RemoteCmd{Command: DefaultRestartCheckCommand}
   226  		var buf, buf2 bytes.Buffer
   227  		cmdModuleLoad.Stdout = &buf
   228  		cmdModuleLoad.Stdout = io.MultiWriter(cmdModuleLoad.Stdout, &buf2)
   229  
   230  		cmdModuleLoad.StartWithUi(p.comm, p.ui)
   231  		stdoutToRead := buf2.String()
   232  
   233  		if !strings.Contains(stdoutToRead, "restarted.") {
   234  			log.Printf("echo didn't succeed; retrying...")
   235  			continue
   236  		}
   237  		break
   238  	}
   239  
   240  	return nil
   241  }
   242  
   243  func (p *Provisioner) Cancel() {
   244  	log.Printf("Received interrupt Cancel()")
   245  
   246  	p.cancelLock.Lock()
   247  	defer p.cancelLock.Unlock()
   248  	if p.cancel != nil {
   249  		close(p.cancel)
   250  	}
   251  }
   252  
   253  // retryable will retry the given function over and over until a
   254  // non-error is returned.
   255  func (p *Provisioner) retryable(f func() error) error {
   256  	startTimeout := time.After(p.config.RestartTimeout)
   257  	for {
   258  		var err error
   259  		if err = f(); err == nil {
   260  			return nil
   261  		}
   262  
   263  		// Create an error and log it
   264  		err = fmt.Errorf("Retryable error: %s", err)
   265  		log.Print(err.Error())
   266  
   267  		// Check if we timed out, otherwise we retry. It is safe to
   268  		// retry since the only error case above is if the command
   269  		// failed to START.
   270  		select {
   271  		case <-startTimeout:
   272  			return err
   273  		default:
   274  			time.Sleep(retryableSleep)
   275  		}
   276  	}
   277  }