github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/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.exe -f -r -t 60"
    24  var AbortReboot = "shutdown.exe -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  
   133  		if cmd.ExitStatus == 1115 || cmd.ExitStatus == 1190 {
   134  			// Reboot already in progress but not completed
   135  			log.Printf("Reboot already in progress, waiting...")
   136  			time.Sleep(10 * time.Second)
   137  		}
   138  		if cmd.ExitStatus == 0 {
   139  			// Cancel reboot we created to test if machine was already rebooting
   140  			cmd = &packer.RemoteCmd{Command: abortcommand}
   141  			cmd.StartWithUi(comm, ui)
   142  			break
   143  		}
   144  	}
   145  
   146  	go func() {
   147  		log.Printf("Waiting for machine to become available...")
   148  		err = waitForCommunicator(p)
   149  		waitDone <- true
   150  	}()
   151  
   152  	log.Printf("Waiting for machine to reboot with timeout: %s", p.config.RestartTimeout)
   153  
   154  WaitLoop:
   155  	for {
   156  		// Wait for either WinRM to become available, a timeout to occur,
   157  		// or an interrupt to come through.
   158  		select {
   159  		case <-waitDone:
   160  			if err != nil {
   161  				ui.Error(fmt.Sprintf("Error waiting for machine to restart: %s", err))
   162  				return err
   163  			}
   164  
   165  			ui.Say("Machine successfully restarted, moving on")
   166  			close(p.cancel)
   167  			break WaitLoop
   168  		case <-timeout:
   169  			err := fmt.Errorf("Timeout waiting for machine to restart.")
   170  			ui.Error(err.Error())
   171  			close(p.cancel)
   172  			return err
   173  		case <-p.cancel:
   174  			close(waitDone)
   175  			return fmt.Errorf("Interrupt detected, quitting waiting for machine to restart")
   176  		}
   177  	}
   178  
   179  	return nil
   180  
   181  }
   182  
   183  var waitForCommunicator = func(p *Provisioner) error {
   184  	runCustomRestartCheck := true
   185  	if p.config.RestartCheckCommand == DefaultRestartCheckCommand {
   186  		runCustomRestartCheck = false
   187  	}
   188  	// This command is configurable by the user to make sure that the
   189  	// vm has met their necessary criteria for having restarted. If the
   190  	// user doesn't set a special restart command, we just run the
   191  	// default as cmdModuleLoad below.
   192  	cmdRestartCheck := &packer.RemoteCmd{Command: p.config.RestartCheckCommand}
   193  	log.Printf("Checking that communicator is connected with: '%s'",
   194  		cmdRestartCheck.Command)
   195  	for {
   196  		select {
   197  		case <-p.cancel:
   198  			log.Println("Communicator wait canceled, exiting loop")
   199  			return fmt.Errorf("Communicator wait canceled")
   200  		case <-time.After(retryableSleep):
   201  		}
   202  		if runCustomRestartCheck {
   203  			// run user-configured restart check
   204  			err := cmdRestartCheck.StartWithUi(p.comm, p.ui)
   205  			if err != nil {
   206  				log.Printf("Communication connection err: %s", err)
   207  				continue
   208  			}
   209  			log.Printf("Connected to machine")
   210  			runCustomRestartCheck = false
   211  		}
   212  
   213  		// This is the non-user-configurable check that powershell
   214  		// modules have loaded.
   215  
   216  		// If we catch the restart in just the right place, we will be able
   217  		// to run the restart check but the output will be an error message
   218  		// about how it needs powershell modules to load, and we will start
   219  		// provisioning before powershell is actually ready.
   220  		// In this next check, we parse stdout to make sure that the command is
   221  		// actually running as expected.
   222  		cmdModuleLoad := &packer.RemoteCmd{Command: DefaultRestartCheckCommand}
   223  		var buf, buf2 bytes.Buffer
   224  		cmdModuleLoad.Stdout = &buf
   225  		cmdModuleLoad.Stdout = io.MultiWriter(cmdModuleLoad.Stdout, &buf2)
   226  
   227  		cmdModuleLoad.StartWithUi(p.comm, p.ui)
   228  		stdoutToRead := buf2.String()
   229  
   230  		if !strings.Contains(stdoutToRead, "restarted.") {
   231  			log.Printf("echo didn't succeed; retrying...")
   232  			continue
   233  		}
   234  		break
   235  	}
   236  
   237  	return nil
   238  }
   239  
   240  func (p *Provisioner) Cancel() {
   241  	log.Printf("Received interrupt Cancel()")
   242  
   243  	p.cancelLock.Lock()
   244  	defer p.cancelLock.Unlock()
   245  	if p.cancel != nil {
   246  		close(p.cancel)
   247  	}
   248  }
   249  
   250  // retryable will retry the given function over and over until a
   251  // non-error is returned.
   252  func (p *Provisioner) retryable(f func() error) error {
   253  	startTimeout := time.After(p.config.RestartTimeout)
   254  	for {
   255  		var err error
   256  		if err = f(); err == nil {
   257  			return nil
   258  		}
   259  
   260  		// Create an error and log it
   261  		err = fmt.Errorf("Retryable error: %s", err)
   262  		log.Print(err.Error())
   263  
   264  		// Check if we timed out, otherwise we retry. It is safe to
   265  		// retry since the only error case above is if the command
   266  		// failed to START.
   267  		select {
   268  		case <-startTimeout:
   269  			return err
   270  		default:
   271  			time.Sleep(retryableSleep)
   272  		}
   273  	}
   274  }