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