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