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