github.com/hashicorp/packer@v1.14.3/provisioner/windows-restart/provisioner.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  //go:generate packer-sdc mapstructure-to-hcl2 -type Config
     5  
     6  package restart
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  
    14  	"log"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/hashicorp/hcl/v2/hcldec"
    20  	"github.com/hashicorp/packer-plugin-sdk/common"
    21  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    22  	"github.com/hashicorp/packer-plugin-sdk/retry"
    23  	"github.com/hashicorp/packer-plugin-sdk/template/config"
    24  	"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
    25  	"github.com/masterzen/winrm"
    26  )
    27  
    28  var DefaultRestartCommand = `shutdown /r /f /t 0 /c "packer restart"`
    29  var DefaultRestartCheckCommand = winrm.Powershell(`echo ("{0} restarted." -f [System.Net.Dns]::GetHostName())`)
    30  var retryableSleep = 5 * time.Second
    31  var TryCheckReboot = `shutdown /r /f /t 60 /c "packer restart test"`
    32  var AbortReboot = `shutdown /a`
    33  
    34  var DefaultRegistryKeys = []string{
    35  	"HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending",
    36  	"HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending",
    37  	"HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress",
    38  }
    39  
    40  type Config struct {
    41  	common.PackerConfig `mapstructure:",squash"`
    42  
    43  	// The command used to restart the guest machine
    44  	RestartCommand string `mapstructure:"restart_command"`
    45  
    46  	// The command to run after executing `restart_command` to check if the guest machine has restarted.
    47  	// This command will retry until the connection to the guest machine has been restored or `restart_timeout` has exceeded.
    48  	// The output of this command will be displayed to the user.
    49  	RestartCheckCommand string `mapstructure:"restart_check_command"`
    50  
    51  	// The timeout for waiting for the machine to restart
    52  	RestartTimeout time.Duration `mapstructure:"restart_timeout"`
    53  
    54  	// Whether to check the registry (see RegistryKeys) for pending reboots
    55  	CheckKey bool `mapstructure:"check_registry"`
    56  
    57  	// custom keys to check for
    58  	RegistryKeys []string `mapstructure:"registry_keys"`
    59  
    60  	ctx interpolate.Context
    61  }
    62  
    63  type Provisioner struct {
    64  	config     Config
    65  	comm       packersdk.Communicator
    66  	ui         packersdk.Ui
    67  	cancel     chan struct{}
    68  	cancelLock sync.Mutex
    69  }
    70  
    71  func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
    72  
    73  func (p *Provisioner) Prepare(raws ...interface{}) error {
    74  	err := config.Decode(&p.config, &config.DecodeOpts{
    75  		PluginType:         "windows-restart",
    76  		Interpolate:        true,
    77  		InterpolateContext: &p.config.ctx,
    78  		InterpolateFilter: &interpolate.RenderFilter{
    79  			Exclude: []string{
    80  				"execute_command",
    81  			},
    82  		},
    83  	}, raws...)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	if p.config.RestartCommand == "" {
    89  		p.config.RestartCommand = DefaultRestartCommand
    90  	}
    91  
    92  	if p.config.RestartCheckCommand == "" {
    93  		p.config.RestartCheckCommand = DefaultRestartCheckCommand
    94  	}
    95  
    96  	if p.config.RestartTimeout == 0 {
    97  		p.config.RestartTimeout = 5 * time.Minute
    98  	}
    99  
   100  	if len(p.config.RegistryKeys) == 0 {
   101  		p.config.RegistryKeys = DefaultRegistryKeys
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, _ map[string]interface{}) error {
   108  	p.cancelLock.Lock()
   109  	p.cancel = make(chan struct{})
   110  	p.cancelLock.Unlock()
   111  
   112  	ui.Say("Restarting Machine")
   113  	p.comm = comm
   114  	p.ui = ui
   115  
   116  	var cmd *packersdk.RemoteCmd
   117  	command := p.config.RestartCommand
   118  	err := retry.Config{StartTimeout: p.config.RestartTimeout}.Run(ctx, func(context.Context) error {
   119  		cmd = &packersdk.RemoteCmd{Command: command}
   120  		return cmd.RunWithUi(ctx, comm, ui)
   121  	})
   122  
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	if cmd.ExitStatus() != 0 && cmd.ExitStatus() != 1115 && cmd.ExitStatus() != 1190 {
   128  		return fmt.Errorf("Restart script exited with non-zero exit status: %d", cmd.ExitStatus())
   129  	}
   130  
   131  	return waitForRestart(ctx, p, comm)
   132  }
   133  
   134  var waitForRestart = func(ctx context.Context, p *Provisioner, comm packersdk.Communicator) error {
   135  	ui := p.ui
   136  	ui.Say("Waiting for machine to restart...")
   137  	waitDone := make(chan bool, 1)
   138  	timeout := time.After(p.config.RestartTimeout)
   139  	var err error
   140  
   141  	p.comm = comm
   142  	var cmd *packersdk.RemoteCmd
   143  	trycommand := TryCheckReboot
   144  	abortcommand := AbortReboot
   145  
   146  	// Stolen from Vagrant reboot checker
   147  	for {
   148  		log.Printf("Check if machine is rebooting...")
   149  		cmd = &packersdk.RemoteCmd{Command: trycommand}
   150  		err = cmd.RunWithUi(ctx, comm, ui)
   151  		if err != nil {
   152  			// Couldn't execute, we assume machine is rebooting already
   153  			break
   154  		}
   155  		if cmd.ExitStatus() == 1 {
   156  			// SSH provisioner, and we're already rebooting. SSH can reconnect
   157  			// without our help; exit this wait loop.
   158  			break
   159  		}
   160  		if cmd.ExitStatus() == 1115 || cmd.ExitStatus() == 1190 || cmd.ExitStatus() == 1717 {
   161  			// Reboot already in progress but not completed
   162  			log.Printf("Reboot already in progress, waiting...")
   163  			time.Sleep(10 * time.Second)
   164  		}
   165  		if cmd.ExitStatus() == 0 {
   166  			// Cancel reboot we created to test if machine was already rebooting
   167  			cmd = &packersdk.RemoteCmd{Command: abortcommand}
   168  			err = cmd.RunWithUi(ctx, comm, ui)
   169  			if err != nil {
   170  				log.Printf("[ERROR] failed to run remote shutdown command: %s, build will likely hang.", err)
   171  			}
   172  			break
   173  		}
   174  	}
   175  
   176  	go func() {
   177  		log.Printf("Waiting for machine to become available...")
   178  		err = waitForCommunicator(ctx, p)
   179  		waitDone <- true
   180  	}()
   181  
   182  	log.Printf("Waiting for machine to reboot with timeout: %s", p.config.RestartTimeout)
   183  
   184  WaitLoop:
   185  	for {
   186  		// Wait for either WinRM to become available, a timeout to occur,
   187  		// or an interrupt to come through.
   188  		select {
   189  		case <-waitDone:
   190  			if err != nil {
   191  				ui.Error(fmt.Sprintf("Error waiting for machine to restart: %s", err))
   192  				return err
   193  			}
   194  
   195  			ui.Say("Machine successfully restarted, moving on")
   196  			close(p.cancel)
   197  			break WaitLoop
   198  		case <-timeout:
   199  			err := fmt.Errorf("Timeout waiting for machine to restart.")
   200  			ui.Error(err.Error())
   201  			close(p.cancel)
   202  			return err
   203  		case <-p.cancel:
   204  			close(waitDone)
   205  			return fmt.Errorf("Interrupt detected, quitting waiting for machine to restart")
   206  		}
   207  	}
   208  	return nil
   209  
   210  }
   211  
   212  var waitForCommunicator = func(ctx context.Context, p *Provisioner) error {
   213  	runCustomRestartCheck := true
   214  	if p.config.RestartCheckCommand == DefaultRestartCheckCommand {
   215  		runCustomRestartCheck = false
   216  	}
   217  	// This command is configurable by the user to make sure that the
   218  	// vm has met their necessary criteria for having restarted. If the
   219  	// user doesn't set a special restart command, we just run the
   220  	// default as cmdModuleLoad below.
   221  	cmdRestartCheck := &packersdk.RemoteCmd{Command: p.config.RestartCheckCommand}
   222  	log.Printf("Checking that communicator is connected with: '%s'",
   223  		cmdRestartCheck.Command)
   224  	for {
   225  		select {
   226  		case <-ctx.Done():
   227  			log.Println("Communicator wait canceled, exiting loop")
   228  			return fmt.Errorf("Communicator wait canceled")
   229  		case <-time.After(retryableSleep):
   230  		}
   231  		if runCustomRestartCheck {
   232  			// run user-configured restart check
   233  			err := cmdRestartCheck.RunWithUi(ctx, p.comm, p.ui)
   234  			if err != nil {
   235  				log.Printf("Communication connection err: %s", err)
   236  				continue
   237  			}
   238  			log.Printf("Connected to machine")
   239  			runCustomRestartCheck = false
   240  		}
   241  		// This is the non-user-configurable check that powershell
   242  		// modules have loaded.
   243  
   244  		// If we catch the restart in just the right place, we will be able
   245  		// to run the restart check but the output will be an error message
   246  		// about how it needs powershell modules to load, and we will start
   247  		// provisioning before powershell is actually ready.
   248  		// In this next check, we parse stdout to make sure that the command is
   249  		// actually running as expected.
   250  		cmdModuleLoad := &packersdk.RemoteCmd{Command: DefaultRestartCheckCommand}
   251  		var buf, buf2 bytes.Buffer
   252  		cmdModuleLoad.Stdout = &buf
   253  		cmdModuleLoad.Stdout = io.MultiWriter(cmdModuleLoad.Stdout, &buf2)
   254  
   255  		err := cmdModuleLoad.RunWithUi(ctx, p.comm, p.ui)
   256  		if err != nil {
   257  			log.Printf("[ERROR] failed to run restart command on guest: %s. Build may hang.", err)
   258  		}
   259  
   260  		stdoutToRead := buf2.String()
   261  
   262  		if !strings.Contains(stdoutToRead, "restarted.") {
   263  			log.Printf("echo didn't succeed; retrying...")
   264  			continue
   265  		}
   266  
   267  		if p.config.CheckKey {
   268  			log.Printf("Connected to machine")
   269  			shouldContinue := false
   270  			for _, RegKey := range p.config.RegistryKeys {
   271  				KeyTestCommand := winrm.Powershell(fmt.Sprintf(`Test-Path "%s"`, RegKey))
   272  				cmdKeyCheck := &packersdk.RemoteCmd{Command: KeyTestCommand}
   273  				log.Printf("Checking registry for pending reboots")
   274  				var buf, buf2 bytes.Buffer
   275  				cmdKeyCheck.Stdout = &buf
   276  				cmdKeyCheck.Stdout = io.MultiWriter(cmdKeyCheck.Stdout, &buf2)
   277  
   278  				err := cmdKeyCheck.RunWithUi(ctx, p.comm, p.ui)
   279  				if err != nil {
   280  					log.Printf("Communication connection err: %s", err)
   281  					shouldContinue = true
   282  				}
   283  
   284  				stdoutToRead := buf2.String()
   285  				if strings.Contains(stdoutToRead, "True") {
   286  					log.Printf("RegistryKey %s exists; waiting...", KeyTestCommand)
   287  					shouldContinue = true
   288  				} else {
   289  					log.Printf("No Registry keys found; exiting wait loop")
   290  				}
   291  			}
   292  			if shouldContinue {
   293  				continue
   294  			}
   295  		}
   296  		break
   297  	}
   298  
   299  	return nil
   300  }