github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/provisioner/windows-restart/provisioner.go (about)

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