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