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