github.com/dacamp/packer@v0.10.2/provisioner/powershell/provisioner.go (about) 1 // This package implements a provisioner for Packer that executes 2 // shell scripts within the remote machine. 3 package powershell 4 5 import ( 6 "bufio" 7 "bytes" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "os" 13 "sort" 14 "strings" 15 "time" 16 17 "github.com/mitchellh/packer/common" 18 "github.com/mitchellh/packer/common/uuid" 19 "github.com/mitchellh/packer/helper/config" 20 "github.com/mitchellh/packer/packer" 21 "github.com/mitchellh/packer/template/interpolate" 22 ) 23 24 const DefaultRemotePath = "c:/Windows/Temp/script.ps1" 25 26 var retryableSleep = 2 * time.Second 27 28 type Config struct { 29 common.PackerConfig `mapstructure:",squash"` 30 31 // If true, the script contains binary and line endings will not be 32 // converted from Windows to Unix-style. 33 Binary bool 34 35 // An inline script to execute. Multiple strings are all executed 36 // in the context of a single shell. 37 Inline []string 38 39 // The local path of the shell script to upload and execute. 40 Script string 41 42 // An array of multiple scripts to run. 43 Scripts []string 44 45 // An array of environment variables that will be injected before 46 // your command(s) are executed. 47 Vars []string `mapstructure:"environment_vars"` 48 49 // The remote path where the local shell script will be uploaded to. 50 // This should be set to a writable file that is in a pre-existing directory. 51 RemotePath string `mapstructure:"remote_path"` 52 53 // The command used to execute the script. The '{{ .Path }}' variable 54 // should be used to specify where the script goes, {{ .Vars }} 55 // can be used to inject the environment_vars into the environment. 56 ExecuteCommand string `mapstructure:"execute_command"` 57 58 // The command used to execute the elevated script. The '{{ .Path }}' variable 59 // should be used to specify where the script goes, {{ .Vars }} 60 // can be used to inject the environment_vars into the environment. 61 ElevatedExecuteCommand string `mapstructure:"elevated_execute_command"` 62 63 // The timeout for retrying to start the process. Until this timeout 64 // is reached, if the provisioner can't start a process, it retries. 65 // This can be set high to allow for reboots. 66 StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"` 67 68 // This is used in the template generation to format environment variables 69 // inside the `ExecuteCommand` template. 70 EnvVarFormat string 71 72 // This is used in the template generation to format environment variables 73 // inside the `ElevatedExecuteCommand` template. 74 ElevatedEnvVarFormat string `mapstructure:"elevated_env_var_format"` 75 76 // Instructs the communicator to run the remote script as a 77 // Windows scheduled task, effectively elevating the remote 78 // user by impersonating a logged-in user 79 ElevatedUser string `mapstructure:"elevated_user"` 80 ElevatedPassword string `mapstructure:"elevated_password"` 81 82 // Valid Exit Codes - 0 is not always the only valid error code! 83 // See http://www.symantec.com/connect/articles/windows-system-error-codes-exit-codes-description for examples 84 // such as 3010 - "The requested operation is successful. Changes will not be effective until the system is rebooted." 85 ValidExitCodes []int `mapstructure:"valid_exit_codes"` 86 87 ctx interpolate.Context 88 } 89 90 type Provisioner struct { 91 config Config 92 communicator packer.Communicator 93 } 94 95 type ExecuteCommandTemplate struct { 96 Vars string 97 Path string 98 } 99 100 func (p *Provisioner) Prepare(raws ...interface{}) error { 101 err := config.Decode(&p.config, &config.DecodeOpts{ 102 Interpolate: true, 103 InterpolateContext: &p.config.ctx, 104 InterpolateFilter: &interpolate.RenderFilter{ 105 Exclude: []string{ 106 "execute_command", 107 }, 108 }, 109 }, raws...) 110 if err != nil { 111 return err 112 } 113 114 if p.config.EnvVarFormat == "" { 115 p.config.EnvVarFormat = `$env:%s=\"%s\"; ` 116 } 117 118 if p.config.ElevatedEnvVarFormat == "" { 119 p.config.ElevatedEnvVarFormat = `$env:%s="%s"; ` 120 } 121 122 if p.config.ExecuteCommand == "" { 123 p.config.ExecuteCommand = `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"` 124 } 125 126 if p.config.ElevatedExecuteCommand == "" { 127 p.config.ElevatedExecuteCommand = `{{.Vars}}{{.Path}}` 128 } 129 130 if p.config.Inline != nil && len(p.config.Inline) == 0 { 131 p.config.Inline = nil 132 } 133 134 if p.config.StartRetryTimeout == 0 { 135 p.config.StartRetryTimeout = 5 * time.Minute 136 } 137 138 if p.config.RemotePath == "" { 139 p.config.RemotePath = DefaultRemotePath 140 } 141 142 if p.config.Scripts == nil { 143 p.config.Scripts = make([]string, 0) 144 } 145 146 if p.config.Vars == nil { 147 p.config.Vars = make([]string, 0) 148 } 149 150 if p.config.ValidExitCodes == nil { 151 p.config.ValidExitCodes = []int{0} 152 } 153 154 var errs error 155 if p.config.Script != "" && len(p.config.Scripts) > 0 { 156 errs = packer.MultiErrorAppend(errs, 157 errors.New("Only one of script or scripts can be specified.")) 158 } 159 160 if p.config.ElevatedUser != "" && p.config.ElevatedPassword == "" { 161 errs = packer.MultiErrorAppend(errs, 162 errors.New("Must supply an 'elevated_password' if 'elevated_user' provided")) 163 } 164 165 if p.config.ElevatedUser == "" && p.config.ElevatedPassword != "" { 166 errs = packer.MultiErrorAppend(errs, 167 errors.New("Must supply an 'elevated_user' if 'elevated_password' provided")) 168 } 169 170 if p.config.Script != "" { 171 p.config.Scripts = []string{p.config.Script} 172 } 173 174 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 175 errs = packer.MultiErrorAppend(errs, 176 errors.New("Either a script file or inline script must be specified.")) 177 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 178 errs = packer.MultiErrorAppend(errs, 179 errors.New("Only a script file or an inline script can be specified, not both.")) 180 } 181 182 for _, path := range p.config.Scripts { 183 if _, err := os.Stat(path); err != nil { 184 errs = packer.MultiErrorAppend(errs, 185 fmt.Errorf("Bad script '%s': %s", path, err)) 186 } 187 } 188 189 // Do a check for bad environment variables, such as '=foo', 'foobar' 190 for _, kv := range p.config.Vars { 191 vs := strings.SplitN(kv, "=", 2) 192 if len(vs) != 2 || vs[0] == "" { 193 errs = packer.MultiErrorAppend(errs, 194 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 195 } 196 } 197 198 if errs != nil { 199 return errs 200 } 201 202 return nil 203 } 204 205 // Takes the inline scripts, concatenates them 206 // into a temporary file and returns a string containing the location 207 // of said file. 208 func extractScript(p *Provisioner) (string, error) { 209 temp, err := ioutil.TempFile(os.TempDir(), "packer-powershell-provisioner") 210 if err != nil { 211 return "", err 212 } 213 defer temp.Close() 214 writer := bufio.NewWriter(temp) 215 for _, command := range p.config.Inline { 216 log.Printf("Found command: %s", command) 217 if _, err := writer.WriteString(command + "\n"); err != nil { 218 return "", fmt.Errorf("Error preparing shell script: %s", err) 219 } 220 } 221 222 if err := writer.Flush(); err != nil { 223 return "", fmt.Errorf("Error preparing shell script: %s", err) 224 } 225 226 return temp.Name(), nil 227 } 228 229 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 230 ui.Say(fmt.Sprintf("Provisioning with Powershell...")) 231 p.communicator = comm 232 233 scripts := make([]string, len(p.config.Scripts)) 234 copy(scripts, p.config.Scripts) 235 236 // Build our variables up by adding in the build name and builder type 237 envVars := make([]string, len(p.config.Vars)+2) 238 envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName 239 envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType 240 copy(envVars, p.config.Vars) 241 242 if p.config.Inline != nil { 243 temp, err := extractScript(p) 244 if err != nil { 245 ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err)) 246 } 247 scripts = append(scripts, temp) 248 } 249 250 for _, path := range scripts { 251 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 252 253 log.Printf("Opening %s for reading", path) 254 f, err := os.Open(path) 255 if err != nil { 256 return fmt.Errorf("Error opening shell script: %s", err) 257 } 258 defer f.Close() 259 260 command, err := p.createCommandText() 261 if err != nil { 262 return fmt.Errorf("Error processing command: %s", err) 263 } 264 265 // Upload the file and run the command. Do this in the context of 266 // a single retryable function so that we don't end up with 267 // the case that the upload succeeded, a restart is initiated, 268 // and then the command is executed but the file doesn't exist 269 // any longer. 270 var cmd *packer.RemoteCmd 271 err = p.retryable(func() error { 272 if _, err := f.Seek(0, 0); err != nil { 273 return err 274 } 275 276 if err := comm.Upload(p.config.RemotePath, f, nil); err != nil { 277 return fmt.Errorf("Error uploading script: %s", err) 278 } 279 280 cmd = &packer.RemoteCmd{Command: command} 281 return cmd.StartWithUi(comm, ui) 282 }) 283 if err != nil { 284 return err 285 } 286 287 // Close the original file since we copied it 288 f.Close() 289 290 // Check exit code against allowed codes (likely just 0) 291 validExitCode := false 292 for _, v := range p.config.ValidExitCodes { 293 if cmd.ExitStatus == v { 294 validExitCode = true 295 } 296 } 297 if !validExitCode { 298 return fmt.Errorf( 299 "Script exited with non-zero exit status: %d. Allowed exit codes are: %v", 300 cmd.ExitStatus, p.config.ValidExitCodes) 301 } 302 } 303 304 return nil 305 } 306 307 func (p *Provisioner) Cancel() { 308 // Just hard quit. It isn't a big deal if what we're doing keeps 309 // running on the other side. 310 os.Exit(0) 311 } 312 313 // retryable will retry the given function over and over until a 314 // non-error is returned. 315 func (p *Provisioner) retryable(f func() error) error { 316 startTimeout := time.After(p.config.StartRetryTimeout) 317 for { 318 var err error 319 if err = f(); err == nil { 320 return nil 321 } 322 323 // Create an error and log it 324 err = fmt.Errorf("Retryable error: %s", err) 325 log.Printf(err.Error()) 326 327 // Check if we timed out, otherwise we retry. It is safe to 328 // retry since the only error case above is if the command 329 // failed to START. 330 select { 331 case <-startTimeout: 332 return err 333 default: 334 time.Sleep(retryableSleep) 335 } 336 } 337 } 338 339 func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, err error) { 340 flattened = "" 341 envVars := make(map[string]string) 342 343 // Always available Packer provided env vars 344 envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName 345 envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType 346 347 // Split vars into key/value components 348 for _, envVar := range p.config.Vars { 349 keyValue := strings.Split(envVar, "=") 350 if len(keyValue) != 2 { 351 err = errors.New("Shell provisioner environment variables must be in key=value format") 352 return 353 } 354 envVars[keyValue[0]] = keyValue[1] 355 } 356 357 // Create a list of env var keys in sorted order 358 var keys []string 359 for k := range envVars { 360 keys = append(keys, k) 361 } 362 sort.Strings(keys) 363 format := p.config.EnvVarFormat 364 if elevated { 365 format = p.config.ElevatedEnvVarFormat 366 } 367 368 // Re-assemble vars using OS specific format pattern and flatten 369 for _, key := range keys { 370 flattened += fmt.Sprintf(format, key, envVars[key]) 371 } 372 return 373 } 374 375 func (p *Provisioner) createCommandText() (command string, err error) { 376 // Create environment variables to set before executing the command 377 flattenedEnvVars, err := p.createFlattenedEnvVars(false) 378 if err != nil { 379 return "", err 380 } 381 382 p.config.ctx.Data = &ExecuteCommandTemplate{ 383 Vars: flattenedEnvVars, 384 Path: p.config.RemotePath, 385 } 386 command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 387 if err != nil { 388 return "", fmt.Errorf("Error processing command: %s", err) 389 } 390 391 // Return the interpolated command 392 if p.config.ElevatedUser == "" { 393 return command, nil 394 } 395 396 // Can't double escape the env vars, lets create shiny new ones 397 flattenedEnvVars, err = p.createFlattenedEnvVars(true) 398 p.config.ctx.Data = &ExecuteCommandTemplate{ 399 Vars: flattenedEnvVars, 400 Path: p.config.RemotePath, 401 } 402 command, err = interpolate.Render(p.config.ElevatedExecuteCommand, &p.config.ctx) 403 if err != nil { 404 return "", fmt.Errorf("Error processing command: %s", err) 405 } 406 407 // OK so we need an elevated shell runner to wrap our command, this is going to have its own path 408 // generate the script and update the command runner in the process 409 path, err := p.generateElevatedRunner(command) 410 411 // Return the path to the elevated shell wrapper 412 command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) 413 414 return 415 } 416 417 func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { 418 log.Printf("Building elevated command wrapper for: %s", command) 419 420 // generate command 421 var buffer bytes.Buffer 422 err = elevatedTemplate.Execute(&buffer, elevatedOptions{ 423 User: p.config.ElevatedUser, 424 Password: p.config.ElevatedPassword, 425 TaskDescription: "Packer elevated task", 426 TaskName: fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()), 427 EncodedCommand: powershellEncode([]byte(command + "; exit $LASTEXITCODE")), 428 }) 429 430 if err != nil { 431 fmt.Printf("Error creating elevated template: %s", err) 432 return "", err 433 } 434 435 tmpFile, err := ioutil.TempFile(os.TempDir(), "packer-elevated-shell.ps1") 436 writer := bufio.NewWriter(tmpFile) 437 if _, err := writer.WriteString(string(buffer.Bytes())); err != nil { 438 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 439 } 440 441 if err := writer.Flush(); err != nil { 442 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 443 } 444 tmpFile.Close() 445 f, err := os.Open(tmpFile.Name()) 446 if err != nil { 447 return "", fmt.Errorf("Error opening temporary elevated shell script: %s", err) 448 } 449 defer f.Close() 450 451 uuid := uuid.TimeOrderedUUID() 452 path := fmt.Sprintf(`${env:TEMP}\packer-elevated-shell-%s.ps1`, uuid) 453 log.Printf("Uploading elevated shell wrapper for command [%s] to [%s] from [%s]", command, path, tmpFile.Name()) 454 err = p.communicator.Upload(path, f, nil) 455 if err != nil { 456 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 457 } 458 459 // CMD formatted Path required for this op 460 path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid) 461 return path, err 462 }