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 }