github.com/kikitux/packer@v0.10.1-0.20160322154024-6237df566f9f/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 configPath, err := p.createConfig( 239 ui, 240 comm, 241 nodeName, 242 serverUrl, 243 p.config.ClientKey, 244 encryptedDataBagSecretPath, 245 remoteValidationKeyPath, 246 p.config.ValidationClientName, 247 p.config.ChefEnvironment, 248 p.config.SslVerifyMode) 249 if err != nil { 250 return fmt.Errorf("Error creating Chef config file: %s", err) 251 } 252 253 if p.config.ValidationKeyPath != "" { 254 remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) 255 if err := p.uploadFile(ui, comm, remoteValidationKeyPath, p.config.ValidationKeyPath); err != nil { 256 return fmt.Errorf("Error copying validation key: %s", err) 257 } 258 } 259 260 jsonPath, err := p.createJson(ui, comm) 261 if err != nil { 262 return fmt.Errorf("Error creating JSON attributes: %s", err) 263 } 264 265 err = p.executeChef(ui, comm, configPath, jsonPath) 266 267 knifeConfigPath, err2 := p.createKnifeConfig( 268 ui, comm, nodeName, serverUrl, p.config.ClientKey, p.config.SslVerifyMode) 269 if err2 != nil { 270 return fmt.Errorf("Error creating knife config on node: %s", err2) 271 } 272 if !p.config.SkipCleanNode { 273 if err2 := p.cleanNode(ui, comm, nodeName, knifeConfigPath); err2 != nil { 274 return fmt.Errorf("Error cleaning up chef node: %s", err2) 275 } 276 } 277 278 if !p.config.SkipCleanClient { 279 if err2 := p.cleanClient(ui, comm, nodeName, knifeConfigPath); err2 != nil { 280 return fmt.Errorf("Error cleaning up chef client: %s", err2) 281 } 282 } 283 284 if err != nil { 285 return fmt.Errorf("Error executing Chef: %s", err) 286 } 287 288 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 289 return fmt.Errorf("Error removing %s: %s", p.config.StagingDir, err) 290 } 291 292 return nil 293 } 294 295 func (p *Provisioner) Cancel() { 296 // Just hard quit. It isn't a big deal if what we're doing keeps 297 // running on the other side. 298 os.Exit(0) 299 } 300 301 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 302 if err := p.createDir(ui, comm, dst); err != nil { 303 return err 304 } 305 306 // Make sure there is a trailing "/" so that the directory isn't 307 // created on the other side. 308 if src[len(src)-1] != '/' { 309 src = src + "/" 310 } 311 312 return comm.UploadDir(dst, src, nil) 313 } 314 315 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error { 316 ui.Message(fmt.Sprintf("Uploading %s...", localPath)) 317 318 f, err := os.Open(localPath) 319 if err != nil { 320 return err 321 } 322 defer f.Close() 323 324 return comm.Upload(remotePath, f, nil) 325 } 326 327 func (p *Provisioner) createConfig( 328 ui packer.Ui, 329 comm packer.Communicator, 330 nodeName string, 331 serverUrl string, 332 clientKey string, 333 encryptedDataBagSecretPath, 334 remoteKeyPath string, 335 validationClientName string, 336 chefEnvironment string, 337 sslVerifyMode string) (string, error) { 338 339 ui.Message("Creating configuration file 'client.rb'") 340 341 // Read the template 342 tpl := DefaultConfigTemplate 343 if p.config.ConfigTemplate != "" { 344 f, err := os.Open(p.config.ConfigTemplate) 345 if err != nil { 346 return "", err 347 } 348 defer f.Close() 349 350 tplBytes, err := ioutil.ReadAll(f) 351 if err != nil { 352 return "", err 353 } 354 355 tpl = string(tplBytes) 356 } 357 358 ctx := p.config.ctx 359 ctx.Data = &ConfigTemplate{ 360 NodeName: nodeName, 361 ServerUrl: serverUrl, 362 ClientKey: clientKey, 363 ValidationKeyPath: remoteKeyPath, 364 ValidationClientName: validationClientName, 365 ChefEnvironment: chefEnvironment, 366 SslVerifyMode: sslVerifyMode, 367 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 368 } 369 configString, err := interpolate.Render(tpl, &ctx) 370 if err != nil { 371 return "", err 372 } 373 374 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb")) 375 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 376 return "", err 377 } 378 379 return remotePath, nil 380 } 381 382 func (p *Provisioner) createKnifeConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, sslVerifyMode string) (string, error) { 383 ui.Message("Creating configuration file 'knife.rb'") 384 385 // Read the template 386 tpl := DefaultKnifeTemplate 387 388 ctx := p.config.ctx 389 ctx.Data = &ConfigTemplate{ 390 NodeName: nodeName, 391 ServerUrl: serverUrl, 392 ClientKey: clientKey, 393 SslVerifyMode: sslVerifyMode, 394 } 395 configString, err := interpolate.Render(tpl, &ctx) 396 if err != nil { 397 return "", err 398 } 399 400 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "knife.rb")) 401 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 402 return "", err 403 } 404 405 return remotePath, nil 406 } 407 408 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 409 ui.Message("Creating JSON attribute file") 410 411 jsonData := make(map[string]interface{}) 412 413 // Copy the configured JSON 414 for k, v := range p.config.Json { 415 jsonData[k] = v 416 } 417 418 // Set the run list if it was specified 419 if len(p.config.RunList) > 0 { 420 jsonData["run_list"] = p.config.RunList 421 } 422 423 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 424 if err != nil { 425 return "", err 426 } 427 428 // Upload the bytes 429 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json")) 430 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 431 return "", err 432 } 433 434 return remotePath, nil 435 } 436 437 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 438 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 439 440 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 441 if err := cmd.StartWithUi(comm, ui); err != nil { 442 return err 443 } 444 if cmd.ExitStatus != 0 { 445 return fmt.Errorf("Non-zero exit status. See output above for more info.") 446 } 447 448 // Chmod the directory to 0777 just so that we can access it as our user 449 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 450 if err := cmd.StartWithUi(comm, ui); err != nil { 451 return err 452 } 453 if cmd.ExitStatus != 0 { 454 return fmt.Errorf("Non-zero exit status. See output above for more info.") 455 } 456 457 return nil 458 } 459 460 func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 461 ui.Say("Cleaning up chef node...") 462 args := []string{"node", "delete", node} 463 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 464 return fmt.Errorf("Failed to cleanup node: %s", err) 465 } 466 467 return nil 468 } 469 470 func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 471 ui.Say("Cleaning up chef client...") 472 args := []string{"client", "delete", node} 473 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 474 return fmt.Errorf("Failed to cleanup client: %s", err) 475 } 476 477 return nil 478 } 479 480 func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string, args []string) error { 481 flags := []string{ 482 "-y", 483 "-c", knifeConfigPath, 484 } 485 486 cmdText := fmt.Sprintf( 487 "knife %s %s", strings.Join(args, " "), strings.Join(flags, " ")) 488 if !p.config.PreventSudo { 489 cmdText = "sudo " + cmdText 490 } 491 492 cmd := &packer.RemoteCmd{Command: cmdText} 493 if err := cmd.StartWithUi(comm, ui); err != nil { 494 return err 495 } 496 if cmd.ExitStatus != 0 { 497 return fmt.Errorf( 498 "Non-zero exit status. See output above for more info.\n\n"+ 499 "Command: %s", 500 cmdText) 501 } 502 503 return nil 504 } 505 506 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 507 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 508 509 cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} 510 if err := cmd.StartWithUi(comm, ui); err != nil { 511 return err 512 } 513 514 return nil 515 } 516 517 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 518 p.config.ctx.Data = &ExecuteTemplate{ 519 ConfigPath: config, 520 JsonPath: json, 521 Sudo: !p.config.PreventSudo, 522 } 523 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 524 if err != nil { 525 return err 526 } 527 528 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 529 530 cmd := &packer.RemoteCmd{ 531 Command: command, 532 } 533 534 if err := cmd.StartWithUi(comm, ui); err != nil { 535 return err 536 } 537 538 if cmd.ExitStatus != 0 { 539 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 540 } 541 542 return nil 543 } 544 545 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { 546 ui.Message("Installing Chef...") 547 548 p.config.ctx.Data = &InstallChefTemplate{ 549 Sudo: !p.config.PreventSudo, 550 } 551 command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx) 552 if err != nil { 553 return err 554 } 555 556 ui.Message(command) 557 558 cmd := &packer.RemoteCmd{Command: command} 559 if err := cmd.StartWithUi(comm, ui); err != nil { 560 return err 561 } 562 563 if cmd.ExitStatus != 0 { 564 return fmt.Errorf( 565 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 566 } 567 568 return nil 569 } 570 571 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 572 if current == nil { 573 return nil, nil 574 } 575 576 switch c := current.(type) { 577 case []interface{}: 578 val := make([]interface{}, len(c)) 579 for i, v := range c { 580 var err error 581 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 582 if err != nil { 583 return nil, err 584 } 585 } 586 587 return val, nil 588 case []uint8: 589 return string(c), nil 590 case map[interface{}]interface{}: 591 val := make(map[string]interface{}) 592 for k, v := range c { 593 ks, ok := k.(string) 594 if !ok { 595 return nil, fmt.Errorf("%s: key is not string", key) 596 } 597 598 var err error 599 val[ks], err = p.deepJsonFix( 600 fmt.Sprintf("%s.%s", key, ks), v) 601 if err != nil { 602 return nil, err 603 } 604 } 605 606 return val, nil 607 default: 608 return current, nil 609 } 610 } 611 612 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 613 jsonBytes, err := json.Marshal(p.config.Json) 614 if err != nil { 615 // This really shouldn't happen since we literally just unmarshalled 616 panic(err) 617 } 618 619 // Copy the user variables so that we can restore them later, and 620 // make sure we make the quotes JSON-friendly in the user variables. 621 originalUserVars := make(map[string]string) 622 for k, v := range p.config.ctx.UserVariables { 623 originalUserVars[k] = v 624 } 625 626 // Make sure we reset them no matter what 627 defer func() { 628 p.config.ctx.UserVariables = originalUserVars 629 }() 630 631 // Make the current user variables JSON string safe. 632 for k, v := range p.config.ctx.UserVariables { 633 v = strings.Replace(v, `\`, `\\`, -1) 634 v = strings.Replace(v, `"`, `\"`, -1) 635 p.config.ctx.UserVariables[k] = v 636 } 637 638 // Process the bytes with the template processor 639 p.config.ctx.Data = nil 640 jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx) 641 if err != nil { 642 return nil, err 643 } 644 645 var result map[string]interface{} 646 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 647 return nil, err 648 } 649 650 return result, nil 651 } 652 653 var DefaultConfigTemplate = ` 654 log_level :info 655 log_location STDOUT 656 chef_server_url "{{.ServerUrl}}" 657 client_key "{{.ClientKey}}" 658 {{if ne .EncryptedDataBagSecretPath ""}} 659 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 660 {{end}} 661 {{if ne .ValidationClientName ""}} 662 validation_client_name "{{.ValidationClientName}}" 663 {{else}} 664 validation_client_name "chef-validator" 665 {{end}} 666 {{if ne .ValidationKeyPath ""}} 667 validation_key "{{.ValidationKeyPath}}" 668 {{end}} 669 node_name "{{.NodeName}}" 670 {{if ne .ChefEnvironment ""}} 671 environment "{{.ChefEnvironment}}" 672 {{end}} 673 {{if ne .SslVerifyMode ""}} 674 ssl_verify_mode :{{.SslVerifyMode}} 675 {{end}} 676 ` 677 678 var DefaultKnifeTemplate = ` 679 log_level :info 680 log_location STDOUT 681 chef_server_url "{{.ServerUrl}}" 682 client_key "{{.ClientKey}}" 683 node_name "{{.NodeName}}" 684 {{if ne .SslVerifyMode ""}} 685 ssl_verify_mode :{{.SslVerifyMode}} 686 {{end}} 687 `