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