github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/provisioner/chef-solo/provisioner.go (about) 1 // This package implements a provisioner for Packer that uses 2 // Chef to provision the remote machine, specifically with chef-solo (that is, 3 // without a Chef server). 4 package chefsolo 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "github.com/mitchellh/packer/common" 11 "github.com/mitchellh/packer/packer" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strings" 16 ) 17 18 type Config struct { 19 common.PackerConfig `mapstructure:",squash"` 20 21 ChefEnvironment string `mapstructure:"chef_environment"` 22 ConfigTemplate string `mapstructure:"config_template"` 23 CookbookPaths []string `mapstructure:"cookbook_paths"` 24 RolesPath string `mapstructure:"roles_path"` 25 DataBagsPath string `mapstructure:"data_bags_path"` 26 EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` 27 EnvironmentsPath string `mapstructure:"environments_path"` 28 ExecuteCommand string `mapstructure:"execute_command"` 29 InstallCommand string `mapstructure:"install_command"` 30 RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"` 31 Json map[string]interface{} 32 PreventSudo bool `mapstructure:"prevent_sudo"` 33 RunList []string `mapstructure:"run_list"` 34 SkipInstall bool `mapstructure:"skip_install"` 35 StagingDir string `mapstructure:"staging_directory"` 36 37 tpl *packer.ConfigTemplate 38 } 39 40 type Provisioner struct { 41 config Config 42 } 43 44 type ConfigTemplate struct { 45 CookbookPaths string 46 DataBagsPath string 47 EncryptedDataBagSecretPath string 48 RolesPath string 49 EnvironmentsPath string 50 ChefEnvironment string 51 52 // Templates don't support boolean statements until Go 1.2. In the 53 // mean time, we do this. 54 // TODO(mitchellh): Remove when Go 1.2 is released 55 HasDataBagsPath bool 56 HasEncryptedDataBagSecretPath bool 57 HasRolesPath bool 58 HasEnvironmentsPath bool 59 } 60 61 type ExecuteTemplate struct { 62 ConfigPath string 63 JsonPath string 64 Sudo bool 65 } 66 67 type InstallChefTemplate struct { 68 Sudo bool 69 } 70 71 func (p *Provisioner) Prepare(raws ...interface{}) error { 72 md, err := common.DecodeConfig(&p.config, raws...) 73 if err != nil { 74 return err 75 } 76 77 p.config.tpl, err = packer.NewConfigTemplate() 78 if err != nil { 79 return err 80 } 81 p.config.tpl.UserVars = p.config.PackerUserVars 82 83 if p.config.ExecuteCommand == "" { 84 p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}" 85 } 86 87 if p.config.InstallCommand == "" { 88 p.config.InstallCommand = "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo {{end}}bash" 89 } 90 91 if p.config.RunList == nil { 92 p.config.RunList = make([]string, 0) 93 } 94 95 if p.config.StagingDir == "" { 96 p.config.StagingDir = "/tmp/packer-chef-solo" 97 } 98 99 // Accumulate any errors 100 errs := common.CheckUnusedConfig(md) 101 102 templates := map[string]*string{ 103 "config_template": &p.config.ConfigTemplate, 104 "data_bags_path": &p.config.DataBagsPath, 105 "encrypted_data_bag_secret": &p.config.EncryptedDataBagSecretPath, 106 "roles_path": &p.config.RolesPath, 107 "staging_dir": &p.config.StagingDir, 108 "environments_path": &p.config.EnvironmentsPath, 109 "chef_environment": &p.config.ChefEnvironment, 110 } 111 112 for n, ptr := range templates { 113 var err error 114 *ptr, err = p.config.tpl.Process(*ptr, nil) 115 if err != nil { 116 errs = packer.MultiErrorAppend( 117 errs, fmt.Errorf("Error processing %s: %s", n, err)) 118 } 119 } 120 121 sliceTemplates := map[string][]string{ 122 "cookbook_paths": p.config.CookbookPaths, 123 "remote_cookbook_paths": p.config.RemoteCookbookPaths, 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 for _, path := range p.config.CookbookPaths { 162 pFileInfo, err := os.Stat(path) 163 164 if err != nil || !pFileInfo.IsDir() { 165 errs = packer.MultiErrorAppend( 166 errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err)) 167 } 168 } 169 170 if p.config.RolesPath != "" { 171 pFileInfo, err := os.Stat(p.config.RolesPath) 172 173 if err != nil || !pFileInfo.IsDir() { 174 errs = packer.MultiErrorAppend( 175 errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err)) 176 } 177 } 178 179 if p.config.DataBagsPath != "" { 180 pFileInfo, err := os.Stat(p.config.DataBagsPath) 181 182 if err != nil || !pFileInfo.IsDir() { 183 errs = packer.MultiErrorAppend( 184 errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, err)) 185 } 186 } 187 188 if p.config.EncryptedDataBagSecretPath != "" { 189 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 190 191 if err != nil || pFileInfo.IsDir() { 192 errs = packer.MultiErrorAppend( 193 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 194 } 195 } 196 197 if p.config.EnvironmentsPath != "" { 198 pFileInfo, err := os.Stat(p.config.EnvironmentsPath) 199 200 if err != nil || !pFileInfo.IsDir() { 201 errs = packer.MultiErrorAppend( 202 errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err)) 203 } 204 } 205 206 jsonValid := true 207 for k, v := range p.config.Json { 208 p.config.Json[k], err = p.deepJsonFix(k, v) 209 if err != nil { 210 errs = packer.MultiErrorAppend( 211 errs, fmt.Errorf("Error processing JSON: %s", err)) 212 jsonValid = false 213 } 214 } 215 216 if jsonValid { 217 // Process the user variables within the JSON and set the JSON. 218 // Do this early so that we can validate and show errors. 219 p.config.Json, err = p.processJsonUserVars() 220 if err != nil { 221 errs = packer.MultiErrorAppend( 222 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 223 } 224 } 225 226 if errs != nil && len(errs.Errors) > 0 { 227 return errs 228 } 229 230 return nil 231 } 232 233 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 234 ui.Say("Provisioning with chef-solo") 235 236 if !p.config.SkipInstall { 237 if err := p.installChef(ui, comm); err != nil { 238 return fmt.Errorf("Error installing Chef: %s", err) 239 } 240 } 241 242 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 243 return fmt.Errorf("Error creating staging directory: %s", err) 244 } 245 246 cookbookPaths := make([]string, 0, len(p.config.CookbookPaths)) 247 for i, path := range p.config.CookbookPaths { 248 targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i) 249 if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil { 250 return fmt.Errorf("Error uploading cookbooks: %s", err) 251 } 252 253 cookbookPaths = append(cookbookPaths, targetPath) 254 } 255 256 rolesPath := "" 257 if p.config.RolesPath != "" { 258 rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir) 259 if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil { 260 return fmt.Errorf("Error uploading roles: %s", err) 261 } 262 } 263 264 dataBagsPath := "" 265 if p.config.DataBagsPath != "" { 266 dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir) 267 if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil { 268 return fmt.Errorf("Error uploading data bags: %s", err) 269 } 270 } 271 272 encryptedDataBagSecretPath := "" 273 if p.config.EncryptedDataBagSecretPath != "" { 274 encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 275 if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil { 276 return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) 277 } 278 } 279 280 environmentsPath := "" 281 if p.config.EnvironmentsPath != "" { 282 environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir) 283 if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil { 284 return fmt.Errorf("Error uploading environments: %s", err) 285 } 286 } 287 288 configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, encryptedDataBagSecretPath, environmentsPath, p.config.ChefEnvironment) 289 if err != nil { 290 return fmt.Errorf("Error creating Chef config file: %s", err) 291 } 292 293 jsonPath, err := p.createJson(ui, comm) 294 if err != nil { 295 return fmt.Errorf("Error creating JSON attributes: %s", err) 296 } 297 298 if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil { 299 return fmt.Errorf("Error executing Chef: %s", err) 300 } 301 302 return nil 303 } 304 305 func (p *Provisioner) Cancel() { 306 // Just hard quit. It isn't a big deal if what we're doing keeps 307 // running on the other side. 308 os.Exit(0) 309 } 310 311 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 312 if err := p.createDir(ui, comm, dst); err != nil { 313 return err 314 } 315 316 // Make sure there is a trailing "/" so that the directory isn't 317 // created on the other side. 318 if src[len(src)-1] != '/' { 319 src = src + "/" 320 } 321 322 return comm.UploadDir(dst, src, nil) 323 } 324 325 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 326 f, err := os.Open(src) 327 if err != nil { 328 return err 329 } 330 defer f.Close() 331 332 return comm.Upload(dst, f, nil) 333 } 334 335 func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, encryptedDataBagSecretPath string, environmentsPath string, chefEnvironment string) (string, error) { 336 ui.Message("Creating configuration file 'solo.rb'") 337 338 cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks)) 339 for i, path := range p.config.RemoteCookbookPaths { 340 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 341 } 342 343 for i, path := range localCookbooks { 344 i = len(p.config.RemoteCookbookPaths) + i 345 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 346 } 347 348 // Read the template 349 tpl := DefaultConfigTemplate 350 if p.config.ConfigTemplate != "" { 351 f, err := os.Open(p.config.ConfigTemplate) 352 if err != nil { 353 return "", err 354 } 355 defer f.Close() 356 357 tplBytes, err := ioutil.ReadAll(f) 358 if err != nil { 359 return "", err 360 } 361 362 tpl = string(tplBytes) 363 } 364 365 configString, err := p.config.tpl.Process(tpl, &ConfigTemplate{ 366 CookbookPaths: strings.Join(cookbook_paths, ","), 367 RolesPath: rolesPath, 368 DataBagsPath: dataBagsPath, 369 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 370 EnvironmentsPath: environmentsPath, 371 HasRolesPath: rolesPath != "", 372 HasDataBagsPath: dataBagsPath != "", 373 HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "", 374 HasEnvironmentsPath: environmentsPath != "", 375 ChefEnvironment: chefEnvironment, 376 }) 377 if err != nil { 378 return "", err 379 } 380 381 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "solo.rb")) 382 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 383 return "", err 384 } 385 386 return remotePath, nil 387 } 388 389 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 390 ui.Message("Creating JSON attribute file") 391 392 jsonData := make(map[string]interface{}) 393 394 // Copy the configured JSON 395 for k, v := range p.config.Json { 396 jsonData[k] = v 397 } 398 399 // Set the run list if it was specified 400 if len(p.config.RunList) > 0 { 401 jsonData["run_list"] = p.config.RunList 402 } 403 404 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 405 if err != nil { 406 return "", err 407 } 408 409 // Upload the bytes 410 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "node.json")) 411 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 412 return "", err 413 } 414 415 return remotePath, nil 416 } 417 418 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 419 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 420 cmd := &packer.RemoteCmd{ 421 Command: fmt.Sprintf("mkdir -p '%s'", dir), 422 } 423 424 if err := cmd.StartWithUi(comm, ui); err != nil { 425 return err 426 } 427 428 if cmd.ExitStatus != 0 { 429 return fmt.Errorf("Non-zero exit status.") 430 } 431 432 return nil 433 } 434 435 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 436 command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteTemplate{ 437 ConfigPath: config, 438 JsonPath: json, 439 Sudo: !p.config.PreventSudo, 440 }) 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 command, err := p.config.tpl.Process(p.config.InstallCommand, &InstallChefTemplate{ 466 Sudo: !p.config.PreventSudo, 467 }) 468 if err != nil { 469 return err 470 } 471 472 cmd := &packer.RemoteCmd{Command: command} 473 if err := cmd.StartWithUi(comm, ui); err != nil { 474 return err 475 } 476 477 if cmd.ExitStatus != 0 { 478 return fmt.Errorf( 479 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 480 } 481 482 return nil 483 } 484 485 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 486 if current == nil { 487 return nil, nil 488 } 489 490 switch c := current.(type) { 491 case []interface{}: 492 val := make([]interface{}, len(c)) 493 for i, v := range c { 494 var err error 495 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 496 if err != nil { 497 return nil, err 498 } 499 } 500 501 return val, nil 502 case []uint8: 503 return string(c), nil 504 case map[interface{}]interface{}: 505 val := make(map[string]interface{}) 506 for k, v := range c { 507 ks, ok := k.(string) 508 if !ok { 509 return nil, fmt.Errorf("%s: key is not string", key) 510 } 511 512 var err error 513 val[ks], err = p.deepJsonFix( 514 fmt.Sprintf("%s.%s", key, ks), v) 515 if err != nil { 516 return nil, err 517 } 518 } 519 520 return val, nil 521 default: 522 return current, nil 523 } 524 } 525 526 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 527 jsonBytes, err := json.Marshal(p.config.Json) 528 if err != nil { 529 // This really shouldn't happen since we literally just unmarshalled 530 panic(err) 531 } 532 533 // Copy the user variables so that we can restore them later, and 534 // make sure we make the quotes JSON-friendly in the user variables. 535 originalUserVars := make(map[string]string) 536 for k, v := range p.config.tpl.UserVars { 537 originalUserVars[k] = v 538 } 539 540 // Make sure we reset them no matter what 541 defer func() { 542 p.config.tpl.UserVars = originalUserVars 543 }() 544 545 // Make the current user variables JSON string safe. 546 for k, v := range p.config.tpl.UserVars { 547 v = strings.Replace(v, `\`, `\\`, -1) 548 v = strings.Replace(v, `"`, `\"`, -1) 549 p.config.tpl.UserVars[k] = v 550 } 551 552 // Process the bytes with the template processor 553 jsonBytesProcessed, err := p.config.tpl.Process(string(jsonBytes), nil) 554 if err != nil { 555 return nil, err 556 } 557 558 var result map[string]interface{} 559 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 560 return nil, err 561 } 562 563 return result, nil 564 } 565 566 var DefaultConfigTemplate = ` 567 cookbook_path [{{.CookbookPaths}}] 568 {{if .HasRolesPath}} 569 role_path "{{.RolesPath}}" 570 {{end}} 571 {{if .HasDataBagsPath}} 572 data_bag_path "{{.DataBagsPath}}" 573 {{end}} 574 {{if .HasEncryptedDataBagSecretPath}} 575 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 576 {{end}} 577 {{if .HasEnvironmentsPath}} 578 environment_path "{{.EnvironmentsPath}}" 579 environment "{{.ChefEnvironment}}" 580 {{end}} 581 `