github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/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 if !p.config.SkipCleanNode { 191 if err2 := p.cleanNode(ui, comm, nodeName); err2 != nil { 192 return fmt.Errorf("Error cleaning up chef node: %s", err2) 193 } 194 } 195 196 if !p.config.SkipCleanClient { 197 if err2 := p.cleanClient(ui, comm, nodeName); err2 != nil { 198 return fmt.Errorf("Error cleaning up chef client: %s", err2) 199 } 200 } 201 202 if err != nil { 203 return fmt.Errorf("Error executing Chef: %s", err) 204 } 205 206 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 207 return fmt.Errorf("Error removing /etc/chef directory: %s", err) 208 } 209 210 return nil 211 } 212 213 func (p *Provisioner) Cancel() { 214 // Just hard quit. It isn't a big deal if what we're doing keeps 215 // running on the other side. 216 os.Exit(0) 217 } 218 219 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 220 if err := p.createDir(ui, comm, dst); err != nil { 221 return err 222 } 223 224 // Make sure there is a trailing "/" so that the directory isn't 225 // created on the other side. 226 if src[len(src)-1] != '/' { 227 src = src + "/" 228 } 229 230 return comm.UploadDir(dst, src, nil) 231 } 232 233 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) { 234 ui.Message("Creating configuration file 'client.rb'") 235 236 // Read the template 237 tpl := DefaultConfigTemplate 238 if p.config.ConfigTemplate != "" { 239 f, err := os.Open(p.config.ConfigTemplate) 240 if err != nil { 241 return "", err 242 } 243 defer f.Close() 244 245 tplBytes, err := ioutil.ReadAll(f) 246 if err != nil { 247 return "", err 248 } 249 250 tpl = string(tplBytes) 251 } 252 253 ctx := p.config.ctx 254 ctx.Data = &ConfigTemplate{ 255 NodeName: nodeName, 256 ServerUrl: serverUrl, 257 ClientKey: clientKey, 258 ValidationKeyPath: remoteKeyPath, 259 ValidationClientName: validationClientName, 260 ChefEnvironment: chefEnvironment, 261 SslVerifyMode: sslVerifyMode, 262 } 263 configString, err := interpolate.Render(tpl, &ctx) 264 if err != nil { 265 return "", err 266 } 267 268 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb")) 269 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 270 return "", err 271 } 272 273 return remotePath, nil 274 } 275 276 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 277 ui.Message("Creating JSON attribute file") 278 279 jsonData := make(map[string]interface{}) 280 281 // Copy the configured JSON 282 for k, v := range p.config.Json { 283 jsonData[k] = v 284 } 285 286 // Set the run list if it was specified 287 if len(p.config.RunList) > 0 { 288 jsonData["run_list"] = p.config.RunList 289 } 290 291 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 292 if err != nil { 293 return "", err 294 } 295 296 // Upload the bytes 297 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json")) 298 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 299 return "", err 300 } 301 302 return remotePath, nil 303 } 304 305 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 306 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 307 308 mkdirCmd := fmt.Sprintf("mkdir -p '%s'", dir) 309 if !p.config.PreventSudo { 310 mkdirCmd = "sudo " + mkdirCmd 311 } 312 313 cmd := &packer.RemoteCmd{Command: mkdirCmd} 314 if err := cmd.StartWithUi(comm, ui); err != nil { 315 return err 316 } 317 if cmd.ExitStatus != 0 { 318 return fmt.Errorf("Non-zero exit status. See output above for more info.") 319 } 320 321 // Chmod the directory to 0777 just so that we can access it as our user 322 mkdirCmd = fmt.Sprintf("chmod 0777 '%s'", dir) 323 if !p.config.PreventSudo { 324 mkdirCmd = "sudo " + mkdirCmd 325 } 326 cmd = &packer.RemoteCmd{Command: mkdirCmd} 327 if err := cmd.StartWithUi(comm, ui); err != nil { 328 return err 329 } 330 if cmd.ExitStatus != 0 { 331 return fmt.Errorf("Non-zero exit status. See output above for more info.") 332 } 333 334 return nil 335 } 336 337 func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string) error { 338 ui.Say("Cleaning up chef node...") 339 args := []string{"node", "delete", node} 340 if err := p.knifeExec(ui, comm, node, args); err != nil { 341 return fmt.Errorf("Failed to cleanup node: %s", err) 342 } 343 344 return nil 345 } 346 347 func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string) error { 348 ui.Say("Cleaning up chef client...") 349 args := []string{"client", "delete", node} 350 if err := p.knifeExec(ui, comm, node, args); err != nil { 351 return fmt.Errorf("Failed to cleanup client: %s", err) 352 } 353 354 return nil 355 } 356 357 func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, args []string) error { 358 flags := []string{ 359 "-y", 360 "-s", fmt.Sprintf("'%s'", p.config.ServerUrl), 361 "-k", fmt.Sprintf("'%s'", p.config.ClientKey), 362 "-u", fmt.Sprintf("'%s'", node), 363 } 364 365 cmdText := fmt.Sprintf( 366 "knife %s %s", strings.Join(args, " "), strings.Join(flags, " ")) 367 if !p.config.PreventSudo { 368 cmdText = "sudo " + cmdText 369 } 370 371 cmd := &packer.RemoteCmd{Command: cmdText} 372 if err := cmd.StartWithUi(comm, ui); err != nil { 373 return err 374 } 375 if cmd.ExitStatus != 0 { 376 return fmt.Errorf( 377 "Non-zero exit status. See output above for more info.\n\n"+ 378 "Command: %s", 379 cmdText) 380 } 381 382 return nil 383 } 384 385 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 386 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 387 388 rmCmd := fmt.Sprintf("rm -rf '%s'", dir) 389 if !p.config.PreventSudo { 390 rmCmd = "sudo " + rmCmd 391 } 392 393 cmd := &packer.RemoteCmd{ 394 Command: rmCmd, 395 } 396 397 if err := cmd.StartWithUi(comm, ui); err != nil { 398 return err 399 } 400 401 return nil 402 } 403 404 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 405 p.config.ctx.Data = &ExecuteTemplate{ 406 ConfigPath: config, 407 JsonPath: json, 408 Sudo: !p.config.PreventSudo, 409 } 410 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 411 if err != nil { 412 return err 413 } 414 415 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 416 417 cmd := &packer.RemoteCmd{ 418 Command: command, 419 } 420 421 if err := cmd.StartWithUi(comm, ui); err != nil { 422 return err 423 } 424 425 if cmd.ExitStatus != 0 { 426 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 427 } 428 429 return nil 430 } 431 432 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { 433 ui.Message("Installing Chef...") 434 435 p.config.ctx.Data = &InstallChefTemplate{ 436 Sudo: !p.config.PreventSudo, 437 } 438 command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx) 439 if err != nil { 440 return err 441 } 442 443 cmd := &packer.RemoteCmd{Command: command} 444 if err := cmd.StartWithUi(comm, ui); err != nil { 445 return err 446 } 447 448 if cmd.ExitStatus != 0 { 449 return fmt.Errorf( 450 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 451 } 452 453 return nil 454 } 455 456 func (p *Provisioner) copyValidationKey(ui packer.Ui, comm packer.Communicator, remotePath string) error { 457 ui.Message("Uploading validation key...") 458 459 // First upload the validation key to a writable location 460 f, err := os.Open(p.config.ValidationKeyPath) 461 if err != nil { 462 return err 463 } 464 defer f.Close() 465 466 if err := comm.Upload(remotePath, f, nil); err != nil { 467 return err 468 } 469 470 return nil 471 } 472 473 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 474 if current == nil { 475 return nil, nil 476 } 477 478 switch c := current.(type) { 479 case []interface{}: 480 val := make([]interface{}, len(c)) 481 for i, v := range c { 482 var err error 483 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 484 if err != nil { 485 return nil, err 486 } 487 } 488 489 return val, nil 490 case []uint8: 491 return string(c), nil 492 case map[interface{}]interface{}: 493 val := make(map[string]interface{}) 494 for k, v := range c { 495 ks, ok := k.(string) 496 if !ok { 497 return nil, fmt.Errorf("%s: key is not string", key) 498 } 499 500 var err error 501 val[ks], err = p.deepJsonFix( 502 fmt.Sprintf("%s.%s", key, ks), v) 503 if err != nil { 504 return nil, err 505 } 506 } 507 508 return val, nil 509 default: 510 return current, nil 511 } 512 } 513 514 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 515 jsonBytes, err := json.Marshal(p.config.Json) 516 if err != nil { 517 // This really shouldn't happen since we literally just unmarshalled 518 panic(err) 519 } 520 521 // Copy the user variables so that we can restore them later, and 522 // make sure we make the quotes JSON-friendly in the user variables. 523 originalUserVars := make(map[string]string) 524 for k, v := range p.config.ctx.UserVariables { 525 originalUserVars[k] = v 526 } 527 528 // Make sure we reset them no matter what 529 defer func() { 530 p.config.ctx.UserVariables = originalUserVars 531 }() 532 533 // Make the current user variables JSON string safe. 534 for k, v := range p.config.ctx.UserVariables { 535 v = strings.Replace(v, `\`, `\\`, -1) 536 v = strings.Replace(v, `"`, `\"`, -1) 537 p.config.ctx.UserVariables[k] = v 538 } 539 540 // Process the bytes with the template processor 541 p.config.ctx.Data = nil 542 jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx) 543 if err != nil { 544 return nil, err 545 } 546 547 var result map[string]interface{} 548 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 549 return nil, err 550 } 551 552 return result, nil 553 } 554 555 var DefaultConfigTemplate = ` 556 log_level :info 557 log_location STDOUT 558 chef_server_url "{{.ServerUrl}}" 559 client_key "{{.ClientKey}}" 560 {{if ne .ValidationClientName ""}} 561 validation_client_name "{{.ValidationClientName}}" 562 {{else}} 563 validation_client_name "chef-validator" 564 {{end}} 565 {{if ne .ValidationKeyPath ""}} 566 validation_key "{{.ValidationKeyPath}}" 567 {{end}} 568 node_name "{{.NodeName}}" 569 {{if ne .ChefEnvironment ""}} 570 environment "{{.ChefEnvironment}}" 571 {{end}} 572 {{if ne .SslVerifyMode ""}} 573 ssl_verify_mode :{{.SslVerifyMode}} 574 {{end}} 575 `