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