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 }