github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/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 111 if err != nil { 112 return err 113 } 114 115 if p.config.EnvVarFormat == "" { 116 p.config.EnvVarFormat = `$env:%s="%s"; ` 117 } 118 119 if p.config.ElevatedEnvVarFormat == "" { 120 p.config.ElevatedEnvVarFormat = `$env:%s="%s"; ` 121 } 122 123 if p.config.ExecuteCommand == "" { 124 p.config.ExecuteCommand = `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` 125 } 126 127 if p.config.ElevatedExecuteCommand == "" { 128 p.config.ElevatedExecuteCommand = `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` 129 } 130 131 if p.config.Inline != nil && len(p.config.Inline) == 0 { 132 p.config.Inline = nil 133 } 134 135 if p.config.StartRetryTimeout == 0 { 136 p.config.StartRetryTimeout = 5 * time.Minute 137 } 138 139 if p.config.RemotePath == "" { 140 p.config.RemotePath = DefaultRemotePath 141 } 142 143 if p.config.Scripts == nil { 144 p.config.Scripts = make([]string, 0) 145 } 146 147 if p.config.Vars == nil { 148 p.config.Vars = make([]string, 0) 149 } 150 151 if p.config.ValidExitCodes == nil { 152 p.config.ValidExitCodes = []int{0} 153 } 154 155 var errs error 156 if p.config.Script != "" && len(p.config.Scripts) > 0 { 157 errs = packer.MultiErrorAppend(errs, 158 errors.New("Only one of script or scripts can be specified.")) 159 } 160 161 if p.config.ElevatedUser != "" && p.config.ElevatedPassword == "" { 162 errs = packer.MultiErrorAppend(errs, 163 errors.New("Must supply an 'elevated_password' if 'elevated_user' provided")) 164 } 165 166 if p.config.ElevatedUser == "" && p.config.ElevatedPassword != "" { 167 errs = packer.MultiErrorAppend(errs, 168 errors.New("Must supply an 'elevated_user' if 'elevated_password' provided")) 169 } 170 171 if p.config.Script != "" { 172 p.config.Scripts = []string{p.config.Script} 173 } 174 175 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 176 errs = packer.MultiErrorAppend(errs, 177 errors.New("Either a script file or inline script must be specified.")) 178 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 179 errs = packer.MultiErrorAppend(errs, 180 errors.New("Only a script file or an inline script can be specified, not both.")) 181 } 182 183 for _, path := range p.config.Scripts { 184 if _, err := os.Stat(path); err != nil { 185 errs = packer.MultiErrorAppend(errs, 186 fmt.Errorf("Bad script '%s': %s", path, err)) 187 } 188 } 189 190 // Do a check for bad environment variables, such as '=foo', 'foobar' 191 for _, kv := range p.config.Vars { 192 vs := strings.SplitN(kv, "=", 2) 193 if len(vs) != 2 || vs[0] == "" { 194 errs = packer.MultiErrorAppend(errs, 195 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 196 } 197 } 198 199 if errs != nil { 200 return errs 201 } 202 203 return nil 204 } 205 206 // Takes the inline scripts, concatenates them 207 // into a temporary file and returns a string containing the location 208 // of said file. 209 func extractScript(p *Provisioner) (string, error) { 210 temp, err := ioutil.TempFile(os.TempDir(), "packer-powershell-provisioner") 211 if err != nil { 212 return "", err 213 } 214 defer temp.Close() 215 writer := bufio.NewWriter(temp) 216 for _, command := range p.config.Inline { 217 log.Printf("Found command: %s", command) 218 if _, err := writer.WriteString(command + "\n"); err != nil { 219 return "", fmt.Errorf("Error preparing shell script: %s", err) 220 } 221 } 222 223 if err := writer.Flush(); err != nil { 224 return "", fmt.Errorf("Error preparing shell script: %s", err) 225 } 226 227 return temp.Name(), nil 228 } 229 230 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 231 ui.Say(fmt.Sprintf("Provisioning with Powershell...")) 232 p.communicator = comm 233 234 scripts := make([]string, len(p.config.Scripts)) 235 copy(scripts, p.config.Scripts) 236 237 // Build our variables up by adding in the build name and builder type 238 envVars := make([]string, len(p.config.Vars)+2) 239 envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName 240 envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType 241 copy(envVars, p.config.Vars) 242 243 if p.config.Inline != nil { 244 temp, err := extractScript(p) 245 if err != nil { 246 ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err)) 247 } 248 scripts = append(scripts, temp) 249 } 250 251 for _, path := range scripts { 252 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 253 254 log.Printf("Opening %s for reading", path) 255 f, err := os.Open(path) 256 if err != nil { 257 return fmt.Errorf("Error opening shell script: %s", err) 258 } 259 defer f.Close() 260 261 command, err := p.createCommandText() 262 if err != nil { 263 return fmt.Errorf("Error processing command: %s", err) 264 } 265 266 // Upload the file and run the command. Do this in the context of 267 // a single retryable function so that we don't end up with 268 // the case that the upload succeeded, a restart is initiated, 269 // and then the command is executed but the file doesn't exist 270 // any longer. 271 var cmd *packer.RemoteCmd 272 err = p.retryable(func() error { 273 if _, err := f.Seek(0, 0); err != nil { 274 return err 275 } 276 277 if err := comm.Upload(p.config.RemotePath, f, nil); err != nil { 278 return fmt.Errorf("Error uploading script: %s", err) 279 } 280 281 cmd = &packer.RemoteCmd{Command: command} 282 return cmd.StartWithUi(comm, ui) 283 }) 284 if err != nil { 285 return err 286 } 287 288 // Close the original file since we copied it 289 f.Close() 290 291 // Check exit code against allowed codes (likely just 0) 292 validExitCode := false 293 for _, v := range p.config.ValidExitCodes { 294 if cmd.ExitStatus == v { 295 validExitCode = true 296 } 297 } 298 if !validExitCode { 299 return fmt.Errorf( 300 "Script exited with non-zero exit status: %d. Allowed exit codes are: %v", 301 cmd.ExitStatus, p.config.ValidExitCodes) 302 } 303 } 304 305 return nil 306 } 307 308 func (p *Provisioner) Cancel() { 309 // Just hard quit. It isn't a big deal if what we're doing keeps 310 // running on the other side. 311 os.Exit(0) 312 } 313 314 // retryable will retry the given function over and over until a 315 // non-error is returned. 316 func (p *Provisioner) retryable(f func() error) error { 317 startTimeout := time.After(p.config.StartRetryTimeout) 318 for { 319 var err error 320 if err = f(); err == nil { 321 return nil 322 } 323 324 // Create an error and log it 325 err = fmt.Errorf("Retryable error: %s", err) 326 log.Printf(err.Error()) 327 328 // Check if we timed out, otherwise we retry. It is safe to 329 // retry since the only error case above is if the command 330 // failed to START. 331 select { 332 case <-startTimeout: 333 return err 334 default: 335 time.Sleep(retryableSleep) 336 } 337 } 338 } 339 340 func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, err error) { 341 flattened = "" 342 envVars := make(map[string]string) 343 344 // Always available Packer provided env vars 345 envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName 346 envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType 347 348 // Split vars into key/value components 349 for _, envVar := range p.config.Vars { 350 keyValue := strings.Split(envVar, "=") 351 352 if len(keyValue) != 2 || keyValue[0] == "" { 353 err = errors.New(fmt.Sprintf("Shell provisioner environment variables must be in key=value format. Currently it is '%s'", envVar)) 354 return 355 } 356 envVars[keyValue[0]] = keyValue[1] 357 } 358 359 // Create a list of env var keys in sorted order 360 var keys []string 361 for k := range envVars { 362 keys = append(keys, k) 363 } 364 sort.Strings(keys) 365 format := p.config.EnvVarFormat 366 if elevated { 367 format = p.config.ElevatedEnvVarFormat 368 } 369 370 // Re-assemble vars using OS specific format pattern and flatten 371 for _, key := range keys { 372 flattened += fmt.Sprintf(format, key, envVars[key]) 373 } 374 return 375 } 376 377 func (p *Provisioner) createCommandText() (command string, err error) { 378 // Return the interpolated command 379 if p.config.ElevatedUser == "" { 380 return p.createCommandTextNonPrivileged() 381 } else { 382 return p.createCommandTextPrivileged() 383 } 384 } 385 386 func (p *Provisioner) createCommandTextNonPrivileged() (command string, err error) { 387 // Create environment variables to set before executing the command 388 flattenedEnvVars, err := p.createFlattenedEnvVars(false) 389 if err != nil { 390 return "", err 391 } 392 p.config.ctx.Data = &ExecuteCommandTemplate{ 393 Vars: flattenedEnvVars, 394 Path: p.config.RemotePath, 395 } 396 command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 397 398 if err != nil { 399 return "", fmt.Errorf("Error processing command: %s", err) 400 } 401 402 commandText, err := p.generateCommandLineRunner(command) 403 if err != nil { 404 return "", fmt.Errorf("Error generating command line runner: %s", err) 405 } 406 407 return commandText, err 408 } 409 410 func (p *Provisioner) generateCommandLineRunner(command string) (commandText string, err error) { 411 log.Printf("Building command line for: %s", command) 412 413 base64EncodedCommand, err := powershellEncode(command) 414 if err != nil { 415 return "", fmt.Errorf("Error encoding command: %s", err) 416 } 417 418 commandText = "powershell -executionpolicy bypass -encodedCommand " + base64EncodedCommand 419 420 return commandText, nil 421 } 422 423 func (p *Provisioner) createCommandTextPrivileged() (command string, err error) { 424 // Can't double escape the env vars, lets create shiny new ones 425 flattenedEnvVars, err := p.createFlattenedEnvVars(true) 426 if err != nil { 427 return "", err 428 } 429 p.config.ctx.Data = &ExecuteCommandTemplate{ 430 Vars: flattenedEnvVars, 431 Path: p.config.RemotePath, 432 } 433 command, err = interpolate.Render(p.config.ElevatedExecuteCommand, &p.config.ctx) 434 if err != nil { 435 return "", fmt.Errorf("Error processing command: %s", err) 436 } 437 438 // OK so we need an elevated shell runner to wrap our command, this is going to have its own path 439 // generate the script and update the command runner in the process 440 path, err := p.generateElevatedRunner(command) 441 if err != nil { 442 return "", fmt.Errorf("Error generating elevated runner: %s", err) 443 } 444 445 // Return the path to the elevated shell wrapper 446 command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) 447 448 return command, err 449 } 450 451 func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { 452 log.Printf("Building elevated command wrapper for: %s", command) 453 454 // generate command 455 var buffer bytes.Buffer 456 457 base64EncodedCommand, err := powershellEncode(command) 458 if err != nil { 459 return "", fmt.Errorf("Error encoding command: %s", err) 460 } 461 462 err = elevatedTemplate.Execute(&buffer, elevatedOptions{ 463 User: p.config.ElevatedUser, 464 Password: p.config.ElevatedPassword, 465 TaskDescription: "Packer elevated task", 466 TaskName: fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()), 467 EncodedCommand: base64EncodedCommand, 468 }) 469 470 if err != nil { 471 fmt.Printf("Error creating elevated template: %s", err) 472 return "", err 473 } 474 475 tmpFile, err := ioutil.TempFile(os.TempDir(), "packer-elevated-shell.ps1") 476 writer := bufio.NewWriter(tmpFile) 477 if _, err := writer.WriteString(string(buffer.Bytes())); err != nil { 478 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 479 } 480 481 if err := writer.Flush(); err != nil { 482 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 483 } 484 tmpFile.Close() 485 f, err := os.Open(tmpFile.Name()) 486 if err != nil { 487 return "", fmt.Errorf("Error opening temporary elevated shell script: %s", err) 488 } 489 defer f.Close() 490 491 uuid := uuid.TimeOrderedUUID() 492 path := fmt.Sprintf(`${env:TEMP}\packer-elevated-shell-%s.ps1`, uuid) 493 log.Printf("Uploading elevated shell wrapper for command [%s] to [%s] from [%s]", command, path, tmpFile.Name()) 494 err = p.communicator.Upload(path, f, nil) 495 if err != nil { 496 return "", fmt.Errorf("Error preparing elevated shell script: %s", err) 497 } 498 499 // CMD formatted Path required for this op 500 path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid) 501 return path, err 502 }