github.com/dacamp/packer@v0.10.2/provisioner/chef-client/provisioner.go (about) 1 // This package implements a provisioner for Packer that uses 2 // Chef to provision the remote machine, specifically with chef-client (that is, 3 // with a Chef server). 4 package chefclient 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/mitchellh/packer/common" 16 "github.com/mitchellh/packer/common/uuid" 17 "github.com/mitchellh/packer/helper/config" 18 "github.com/mitchellh/packer/packer" 19 "github.com/mitchellh/packer/provisioner" 20 "github.com/mitchellh/packer/template/interpolate" 21 ) 22 23 type guestOSTypeConfig struct { 24 executeCommand string 25 installCommand string 26 stagingDir string 27 } 28 29 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 30 provisioner.UnixOSType: guestOSTypeConfig{ 31 executeCommand: "{{if .Sudo}}sudo {{end}}chef-client --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 32 installCommand: "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash", 33 stagingDir: "/tmp/packer-chef-client", 34 }, 35 provisioner.WindowsOSType: guestOSTypeConfig{ 36 executeCommand: "c:/opscode/chef/bin/chef-client.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 37 installCommand: "powershell.exe -Command \"(New-Object System.Net.WebClient).DownloadFile('http://chef.io/chef/install.msi', 'C:\\Windows\\Temp\\chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait\"", 38 stagingDir: "C:/Windows/Temp/packer-chef-client", 39 }, 40 } 41 42 type Config struct { 43 common.PackerConfig `mapstructure:",squash"` 44 45 Json map[string]interface{} 46 47 ChefEnvironment string `mapstructure:"chef_environment"` 48 ClientKey string `mapstructure:"client_key"` 49 ConfigTemplate string `mapstructure:"config_template"` 50 EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` 51 ExecuteCommand string `mapstructure:"execute_command"` 52 GuestOSType string `mapstructure:"guest_os_type"` 53 InstallCommand string `mapstructure:"install_command"` 54 NodeName string `mapstructure:"node_name"` 55 PreventSudo bool `mapstructure:"prevent_sudo"` 56 RunList []string `mapstructure:"run_list"` 57 ServerUrl string `mapstructure:"server_url"` 58 SkipCleanClient bool `mapstructure:"skip_clean_client"` 59 SkipCleanNode bool `mapstructure:"skip_clean_node"` 60 SkipInstall bool `mapstructure:"skip_install"` 61 SslVerifyMode string `mapstructure:"ssl_verify_mode"` 62 StagingDir string `mapstructure:"staging_directory"` 63 ValidationClientName string `mapstructure:"validation_client_name"` 64 ValidationKeyPath string `mapstructure:"validation_key_path"` 65 66 ctx interpolate.Context 67 } 68 69 type Provisioner struct { 70 config Config 71 guestOSTypeConfig guestOSTypeConfig 72 guestCommands *provisioner.GuestCommands 73 } 74 75 type ConfigTemplate struct { 76 ChefEnvironment string 77 ClientKey string 78 EncryptedDataBagSecretPath string 79 NodeName string 80 ServerUrl string 81 SslVerifyMode string 82 ValidationClientName string 83 ValidationKeyPath string 84 } 85 86 type ExecuteTemplate struct { 87 ConfigPath string 88 JsonPath string 89 Sudo bool 90 } 91 92 type InstallChefTemplate struct { 93 Sudo bool 94 } 95 96 func (p *Provisioner) Prepare(raws ...interface{}) error { 97 err := config.Decode(&p.config, &config.DecodeOpts{ 98 Interpolate: true, 99 InterpolateContext: &p.config.ctx, 100 InterpolateFilter: &interpolate.RenderFilter{ 101 Exclude: []string{ 102 "execute_command", 103 "install_command", 104 }, 105 }, 106 }, raws...) 107 if err != nil { 108 return err 109 } 110 111 if p.config.GuestOSType == "" { 112 p.config.GuestOSType = provisioner.DefaultOSType 113 } 114 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 115 116 var ok bool 117 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 118 if !ok { 119 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 120 } 121 122 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) 123 if err != nil { 124 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 125 } 126 127 if p.config.ExecuteCommand == "" { 128 p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand 129 } 130 131 if p.config.InstallCommand == "" { 132 p.config.InstallCommand = p.guestOSTypeConfig.installCommand 133 } 134 135 if p.config.RunList == nil { 136 p.config.RunList = make([]string, 0) 137 } 138 139 if p.config.StagingDir == "" { 140 p.config.StagingDir = p.guestOSTypeConfig.stagingDir 141 } 142 143 var errs *packer.MultiError 144 if p.config.ConfigTemplate != "" { 145 fi, err := os.Stat(p.config.ConfigTemplate) 146 if err != nil { 147 errs = packer.MultiErrorAppend( 148 errs, fmt.Errorf("Bad config template path: %s", err)) 149 } else if fi.IsDir() { 150 errs = packer.MultiErrorAppend( 151 errs, fmt.Errorf("Config template path must be a file: %s", err)) 152 } 153 } 154 155 if p.config.EncryptedDataBagSecretPath != "" { 156 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 157 158 if err != nil || pFileInfo.IsDir() { 159 errs = packer.MultiErrorAppend( 160 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 161 } 162 } 163 164 if p.config.ServerUrl == "" { 165 errs = packer.MultiErrorAppend( 166 errs, fmt.Errorf("server_url must be set")) 167 } 168 169 if p.config.EncryptedDataBagSecretPath != "" { 170 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 171 172 if err != nil || pFileInfo.IsDir() { 173 errs = packer.MultiErrorAppend( 174 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 175 } 176 } 177 178 jsonValid := true 179 for k, v := range p.config.Json { 180 p.config.Json[k], err = p.deepJsonFix(k, v) 181 if err != nil { 182 errs = packer.MultiErrorAppend( 183 errs, fmt.Errorf("Error processing JSON: %s", err)) 184 jsonValid = false 185 } 186 } 187 188 if jsonValid { 189 // Process the user variables within the JSON and set the JSON. 190 // Do this early so that we can validate and show errors. 191 p.config.Json, err = p.processJsonUserVars() 192 if err != nil { 193 errs = packer.MultiErrorAppend( 194 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 195 } 196 } 197 198 if errs != nil && len(errs.Errors) > 0 { 199 return errs 200 } 201 202 return nil 203 } 204 205 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 206 207 nodeName := p.config.NodeName 208 if nodeName == "" { 209 nodeName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) 210 } 211 remoteValidationKeyPath := "" 212 serverUrl := p.config.ServerUrl 213 214 if !p.config.SkipInstall { 215 if err := p.installChef(ui, comm); err != nil { 216 return fmt.Errorf("Error installing Chef: %s", err) 217 } 218 } 219 220 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 221 return fmt.Errorf("Error creating staging directory: %s", err) 222 } 223 224 if p.config.ClientKey == "" { 225 p.config.ClientKey = fmt.Sprintf("%s/client.pem", p.config.StagingDir) 226 } 227 228 encryptedDataBagSecretPath := "" 229 if p.config.EncryptedDataBagSecretPath != "" { 230 encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 231 if err := p.uploadFile(ui, 232 comm, 233 encryptedDataBagSecretPath, 234 p.config.EncryptedDataBagSecretPath); err != nil { 235 return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) 236 } 237 } 238 239 if p.config.ValidationKeyPath != "" { 240 remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) 241 if err := p.uploadFile(ui, comm, remoteValidationKeyPath, p.config.ValidationKeyPath); err != nil { 242 return fmt.Errorf("Error copying validation key: %s", err) 243 } 244 } 245 246 configPath, err := p.createConfig( 247 ui, 248 comm, 249 nodeName, 250 serverUrl, 251 p.config.ClientKey, 252 encryptedDataBagSecretPath, 253 remoteValidationKeyPath, 254 p.config.ValidationClientName, 255 p.config.ChefEnvironment, 256 p.config.SslVerifyMode) 257 if err != nil { 258 return fmt.Errorf("Error creating Chef config file: %s", err) 259 } 260 261 jsonPath, err := p.createJson(ui, comm) 262 if err != nil { 263 return fmt.Errorf("Error creating JSON attributes: %s", err) 264 } 265 266 err = p.executeChef(ui, comm, configPath, jsonPath) 267 268 knifeConfigPath, err2 := p.createKnifeConfig( 269 ui, comm, nodeName, serverUrl, p.config.ClientKey, p.config.SslVerifyMode) 270 if err2 != nil { 271 return fmt.Errorf("Error creating knife config on node: %s", err2) 272 } 273 if !p.config.SkipCleanNode { 274 if err2 := p.cleanNode(ui, comm, nodeName, knifeConfigPath); err2 != nil { 275 return fmt.Errorf("Error cleaning up chef node: %s", err2) 276 } 277 } 278 279 if !p.config.SkipCleanClient { 280 if err2 := p.cleanClient(ui, comm, nodeName, knifeConfigPath); err2 != nil { 281 return fmt.Errorf("Error cleaning up chef client: %s", err2) 282 } 283 } 284 285 if err != nil { 286 return fmt.Errorf("Error executing Chef: %s", err) 287 } 288 289 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 290 return fmt.Errorf("Error removing %s: %s", p.config.StagingDir, err) 291 } 292 293 return nil 294 } 295 296 func (p *Provisioner) Cancel() { 297 // Just hard quit. It isn't a big deal if what we're doing keeps 298 // running on the other side. 299 os.Exit(0) 300 } 301 302 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 303 if err := p.createDir(ui, comm, dst); err != nil { 304 return err 305 } 306 307 // Make sure there is a trailing "/" so that the directory isn't 308 // created on the other side. 309 if src[len(src)-1] != '/' { 310 src = src + "/" 311 } 312 313 return comm.UploadDir(dst, src, nil) 314 } 315 316 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error { 317 ui.Message(fmt.Sprintf("Uploading %s...", localPath)) 318 319 f, err := os.Open(localPath) 320 if err != nil { 321 return err 322 } 323 defer f.Close() 324 325 return comm.Upload(remotePath, f, nil) 326 } 327 328 func (p *Provisioner) createConfig( 329 ui packer.Ui, 330 comm packer.Communicator, 331 nodeName string, 332 serverUrl string, 333 clientKey string, 334 encryptedDataBagSecretPath, 335 remoteKeyPath string, 336 validationClientName string, 337 chefEnvironment string, 338 sslVerifyMode string) (string, error) { 339 340 ui.Message("Creating configuration file 'client.rb'") 341 342 // Read the template 343 tpl := DefaultConfigTemplate 344 if p.config.ConfigTemplate != "" { 345 f, err := os.Open(p.config.ConfigTemplate) 346 if err != nil { 347 return "", err 348 } 349 defer f.Close() 350 351 tplBytes, err := ioutil.ReadAll(f) 352 if err != nil { 353 return "", err 354 } 355 356 tpl = string(tplBytes) 357 } 358 359 ctx := p.config.ctx 360 ctx.Data = &ConfigTemplate{ 361 NodeName: nodeName, 362 ServerUrl: serverUrl, 363 ClientKey: clientKey, 364 ValidationKeyPath: remoteKeyPath, 365 ValidationClientName: validationClientName, 366 ChefEnvironment: chefEnvironment, 367 SslVerifyMode: sslVerifyMode, 368 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 369 } 370 configString, err := interpolate.Render(tpl, &ctx) 371 if err != nil { 372 return "", err 373 } 374 375 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb")) 376 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 377 return "", err 378 } 379 380 return remotePath, nil 381 } 382 383 func (p *Provisioner) createKnifeConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, sslVerifyMode string) (string, error) { 384 ui.Message("Creating configuration file 'knife.rb'") 385 386 // Read the template 387 tpl := DefaultKnifeTemplate 388 389 ctx := p.config.ctx 390 ctx.Data = &ConfigTemplate{ 391 NodeName: nodeName, 392 ServerUrl: serverUrl, 393 ClientKey: clientKey, 394 SslVerifyMode: sslVerifyMode, 395 } 396 configString, err := interpolate.Render(tpl, &ctx) 397 if err != nil { 398 return "", err 399 } 400 401 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "knife.rb")) 402 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 403 return "", err 404 } 405 406 return remotePath, nil 407 } 408 409 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 410 ui.Message("Creating JSON attribute file") 411 412 jsonData := make(map[string]interface{}) 413 414 // Copy the configured JSON 415 for k, v := range p.config.Json { 416 jsonData[k] = v 417 } 418 419 // Set the run list if it was specified 420 if len(p.config.RunList) > 0 { 421 jsonData["run_list"] = p.config.RunList 422 } 423 424 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 425 if err != nil { 426 return "", err 427 } 428 429 // Upload the bytes 430 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json")) 431 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 432 return "", err 433 } 434 435 return remotePath, nil 436 } 437 438 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 439 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 440 441 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 442 if err := cmd.StartWithUi(comm, ui); err != nil { 443 return err 444 } 445 if cmd.ExitStatus != 0 { 446 return fmt.Errorf("Non-zero exit status. See output above for more info.") 447 } 448 449 // Chmod the directory to 0777 just so that we can access it as our user 450 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 451 if err := cmd.StartWithUi(comm, ui); err != nil { 452 return err 453 } 454 if cmd.ExitStatus != 0 { 455 return fmt.Errorf("Non-zero exit status. See output above for more info.") 456 } 457 458 return nil 459 } 460 461 func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 462 ui.Say("Cleaning up chef node...") 463 args := []string{"node", "delete", node} 464 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 465 return fmt.Errorf("Failed to cleanup node: %s", err) 466 } 467 468 return nil 469 } 470 471 func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 472 ui.Say("Cleaning up chef client...") 473 args := []string{"client", "delete", node} 474 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 475 return fmt.Errorf("Failed to cleanup client: %s", err) 476 } 477 478 return nil 479 } 480 481 func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string, args []string) error { 482 flags := []string{ 483 "-y", 484 "-c", knifeConfigPath, 485 } 486 487 cmdText := fmt.Sprintf( 488 "knife %s %s", strings.Join(args, " "), strings.Join(flags, " ")) 489 if !p.config.PreventSudo { 490 cmdText = "sudo " + cmdText 491 } 492 493 cmd := &packer.RemoteCmd{Command: cmdText} 494 if err := cmd.StartWithUi(comm, ui); err != nil { 495 return err 496 } 497 if cmd.ExitStatus != 0 { 498 return fmt.Errorf( 499 "Non-zero exit status. See output above for more info.\n\n"+ 500 "Command: %s", 501 cmdText) 502 } 503 504 return nil 505 } 506 507 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 508 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 509 510 cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} 511 if err := cmd.StartWithUi(comm, ui); err != nil { 512 return err 513 } 514 515 return nil 516 } 517 518 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 519 p.config.ctx.Data = &ExecuteTemplate{ 520 ConfigPath: config, 521 JsonPath: json, 522 Sudo: !p.config.PreventSudo, 523 } 524 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 525 if err != nil { 526 return err 527 } 528 529 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 530 531 cmd := &packer.RemoteCmd{ 532 Command: command, 533 } 534 535 if err := cmd.StartWithUi(comm, ui); err != nil { 536 return err 537 } 538 539 if cmd.ExitStatus != 0 { 540 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 541 } 542 543 return nil 544 } 545 546 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { 547 ui.Message("Installing Chef...") 548 549 p.config.ctx.Data = &InstallChefTemplate{ 550 Sudo: !p.config.PreventSudo, 551 } 552 command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx) 553 if err != nil { 554 return err 555 } 556 557 ui.Message(command) 558 559 cmd := &packer.RemoteCmd{Command: command} 560 if err := cmd.StartWithUi(comm, ui); err != nil { 561 return err 562 } 563 564 if cmd.ExitStatus != 0 { 565 return fmt.Errorf( 566 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 567 } 568 569 return nil 570 } 571 572 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 573 if current == nil { 574 return nil, nil 575 } 576 577 switch c := current.(type) { 578 case []interface{}: 579 val := make([]interface{}, len(c)) 580 for i, v := range c { 581 var err error 582 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 583 if err != nil { 584 return nil, err 585 } 586 } 587 588 return val, nil 589 case []uint8: 590 return string(c), nil 591 case map[interface{}]interface{}: 592 val := make(map[string]interface{}) 593 for k, v := range c { 594 ks, ok := k.(string) 595 if !ok { 596 return nil, fmt.Errorf("%s: key is not string", key) 597 } 598 599 var err error 600 val[ks], err = p.deepJsonFix( 601 fmt.Sprintf("%s.%s", key, ks), v) 602 if err != nil { 603 return nil, err 604 } 605 } 606 607 return val, nil 608 default: 609 return current, nil 610 } 611 } 612 613 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 614 jsonBytes, err := json.Marshal(p.config.Json) 615 if err != nil { 616 // This really shouldn't happen since we literally just unmarshalled 617 panic(err) 618 } 619 620 // Copy the user variables so that we can restore them later, and 621 // make sure we make the quotes JSON-friendly in the user variables. 622 originalUserVars := make(map[string]string) 623 for k, v := range p.config.ctx.UserVariables { 624 originalUserVars[k] = v 625 } 626 627 // Make sure we reset them no matter what 628 defer func() { 629 p.config.ctx.UserVariables = originalUserVars 630 }() 631 632 // Make the current user variables JSON string safe. 633 for k, v := range p.config.ctx.UserVariables { 634 v = strings.Replace(v, `\`, `\\`, -1) 635 v = strings.Replace(v, `"`, `\"`, -1) 636 p.config.ctx.UserVariables[k] = v 637 } 638 639 // Process the bytes with the template processor 640 p.config.ctx.Data = nil 641 jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx) 642 if err != nil { 643 return nil, err 644 } 645 646 var result map[string]interface{} 647 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 648 return nil, err 649 } 650 651 return result, nil 652 } 653 654 var DefaultConfigTemplate = ` 655 log_level :info 656 log_location STDOUT 657 chef_server_url "{{.ServerUrl}}" 658 client_key "{{.ClientKey}}" 659 {{if ne .EncryptedDataBagSecretPath ""}} 660 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 661 {{end}} 662 {{if ne .ValidationClientName ""}} 663 validation_client_name "{{.ValidationClientName}}" 664 {{else}} 665 validation_client_name "chef-validator" 666 {{end}} 667 {{if ne .ValidationKeyPath ""}} 668 validation_key "{{.ValidationKeyPath}}" 669 {{end}} 670 node_name "{{.NodeName}}" 671 {{if ne .ChefEnvironment ""}} 672 environment "{{.ChefEnvironment}}" 673 {{end}} 674 {{if ne .SslVerifyMode ""}} 675 ssl_verify_mode :{{.SslVerifyMode}} 676 {{end}} 677 ` 678 679 var DefaultKnifeTemplate = ` 680 log_level :info 681 log_location STDOUT 682 chef_server_url "{{.ServerUrl}}" 683 client_key "{{.ClientKey}}" 684 node_name "{{.NodeName}}" 685 {{if ne .SslVerifyMode ""}} 686 ssl_verify_mode :{{.SslVerifyMode}} 687 {{end}} 688 `