github.com/jbronn/packer@v0.1.6-0.20140120165540-8a1364dbd817/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 // Process the user variables within the JSON and set the JSON. 207 // Do this early so that we can validate and show errors. 208 p.config.Json, err = p.processJsonUserVars() 209 if err != nil { 210 errs = packer.MultiErrorAppend( 211 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 212 } 213 214 if errs != nil && len(errs.Errors) > 0 { 215 return errs 216 } 217 218 return nil 219 } 220 221 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 222 ui.Say("Provisioning with chef-solo") 223 224 if !p.config.SkipInstall { 225 if err := p.installChef(ui, comm); err != nil { 226 return fmt.Errorf("Error installing Chef: %s", err) 227 } 228 } 229 230 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 231 return fmt.Errorf("Error creating staging directory: %s", err) 232 } 233 234 cookbookPaths := make([]string, 0, len(p.config.CookbookPaths)) 235 for i, path := range p.config.CookbookPaths { 236 targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i) 237 if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil { 238 return fmt.Errorf("Error uploading cookbooks: %s", err) 239 } 240 241 cookbookPaths = append(cookbookPaths, targetPath) 242 } 243 244 rolesPath := "" 245 if p.config.RolesPath != "" { 246 rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir) 247 if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil { 248 return fmt.Errorf("Error uploading roles: %s", err) 249 } 250 } 251 252 dataBagsPath := "" 253 if p.config.DataBagsPath != "" { 254 dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir) 255 if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil { 256 return fmt.Errorf("Error uploading data bags: %s", err) 257 } 258 } 259 260 encryptedDataBagSecretPath := "" 261 if p.config.EncryptedDataBagSecretPath != "" { 262 encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 263 if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil { 264 return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) 265 } 266 } 267 268 environmentsPath := "" 269 if p.config.EnvironmentsPath != "" { 270 environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir) 271 if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil { 272 return fmt.Errorf("Error uploading environments: %s", err) 273 } 274 } 275 276 configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, encryptedDataBagSecretPath, environmentsPath, p.config.ChefEnvironment) 277 if err != nil { 278 return fmt.Errorf("Error creating Chef config file: %s", err) 279 } 280 281 jsonPath, err := p.createJson(ui, comm) 282 if err != nil { 283 return fmt.Errorf("Error creating JSON attributes: %s", err) 284 } 285 286 if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil { 287 return fmt.Errorf("Error executing Chef: %s", err) 288 } 289 290 return nil 291 } 292 293 func (p *Provisioner) Cancel() { 294 // Just hard quit. It isn't a big deal if what we're doing keeps 295 // running on the other side. 296 os.Exit(0) 297 } 298 299 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 300 if err := p.createDir(ui, comm, dst); err != nil { 301 return err 302 } 303 304 // Make sure there is a trailing "/" so that the directory isn't 305 // created on the other side. 306 if src[len(src)-1] != '/' { 307 src = src + "/" 308 } 309 310 return comm.UploadDir(dst, src, nil) 311 } 312 313 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 314 f, err := os.Open(src) 315 if err != nil { 316 return err 317 } 318 defer f.Close() 319 320 return comm.Upload(dst, f) 321 } 322 323 func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, encryptedDataBagSecretPath string, environmentsPath string, chefEnvironment string) (string, error) { 324 ui.Message("Creating configuration file 'solo.rb'") 325 326 cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks)) 327 for i, path := range p.config.RemoteCookbookPaths { 328 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 329 } 330 331 for i, path := range localCookbooks { 332 i = len(p.config.RemoteCookbookPaths) + i 333 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 334 } 335 336 // Read the template 337 tpl := DefaultConfigTemplate 338 if p.config.ConfigTemplate != "" { 339 f, err := os.Open(p.config.ConfigTemplate) 340 if err != nil { 341 return "", err 342 } 343 defer f.Close() 344 345 tplBytes, err := ioutil.ReadAll(f) 346 if err != nil { 347 return "", err 348 } 349 350 tpl = string(tplBytes) 351 } 352 353 configString, err := p.config.tpl.Process(tpl, &ConfigTemplate{ 354 CookbookPaths: strings.Join(cookbook_paths, ","), 355 RolesPath: rolesPath, 356 DataBagsPath: dataBagsPath, 357 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 358 EnvironmentsPath: environmentsPath, 359 HasRolesPath: rolesPath != "", 360 HasDataBagsPath: dataBagsPath != "", 361 HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "", 362 HasEnvironmentsPath: environmentsPath != "", 363 ChefEnvironment: chefEnvironment, 364 }) 365 if err != nil { 366 return "", err 367 } 368 369 remotePath := filepath.Join(p.config.StagingDir, "solo.rb") 370 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString))); err != nil { 371 return "", err 372 } 373 374 return remotePath, nil 375 } 376 377 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 378 ui.Message("Creating JSON attribute file") 379 380 jsonData := make(map[string]interface{}) 381 382 // Copy the configured JSON 383 for k, v := range p.config.Json { 384 jsonData[k] = v 385 } 386 387 // Set the run list if it was specified 388 if len(p.config.RunList) > 0 { 389 jsonData["run_list"] = p.config.RunList 390 } 391 392 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 393 if err != nil { 394 return "", err 395 } 396 397 // Upload the bytes 398 remotePath := filepath.Join(p.config.StagingDir, "node.json") 399 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes)); err != nil { 400 return "", err 401 } 402 403 return remotePath, nil 404 } 405 406 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 407 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 408 cmd := &packer.RemoteCmd{ 409 Command: fmt.Sprintf("mkdir -p '%s'", dir), 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.") 418 } 419 420 return nil 421 } 422 423 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 424 command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteTemplate{ 425 ConfigPath: config, 426 JsonPath: json, 427 Sudo: !p.config.PreventSudo, 428 }) 429 if err != nil { 430 return err 431 } 432 433 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 434 435 cmd := &packer.RemoteCmd{ 436 Command: command, 437 } 438 439 if err := cmd.StartWithUi(comm, ui); err != nil { 440 return err 441 } 442 443 if cmd.ExitStatus != 0 { 444 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 445 } 446 447 return nil 448 } 449 450 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { 451 ui.Message("Installing Chef...") 452 453 command, err := p.config.tpl.Process(p.config.InstallCommand, &InstallChefTemplate{ 454 Sudo: !p.config.PreventSudo, 455 }) 456 if err != nil { 457 return err 458 } 459 460 cmd := &packer.RemoteCmd{Command: command} 461 if err := cmd.StartWithUi(comm, ui); err != nil { 462 return err 463 } 464 465 if cmd.ExitStatus != 0 { 466 return fmt.Errorf( 467 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 468 } 469 470 return nil 471 } 472 473 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 474 jsonBytes, err := json.Marshal(p.config.Json) 475 if err != nil { 476 // This really shouldn't happen since we literally just unmarshalled 477 panic(err) 478 } 479 480 // Copy the user variables so that we can restore them later, and 481 // make sure we make the quotes JSON-friendly in the user variables. 482 originalUserVars := make(map[string]string) 483 for k, v := range p.config.tpl.UserVars { 484 originalUserVars[k] = v 485 } 486 487 // Make sure we reset them no matter what 488 defer func() { 489 p.config.tpl.UserVars = originalUserVars 490 }() 491 492 // Make the current user variables JSON string safe. 493 for k, v := range p.config.tpl.UserVars { 494 v = strings.Replace(v, `\`, `\\`, -1) 495 v = strings.Replace(v, `"`, `\"`, -1) 496 p.config.tpl.UserVars[k] = v 497 } 498 499 // Process the bytes with the template processor 500 jsonBytesProcessed, err := p.config.tpl.Process(string(jsonBytes), nil) 501 if err != nil { 502 return nil, err 503 } 504 505 var result map[string]interface{} 506 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 507 return nil, err 508 } 509 510 return result, nil 511 } 512 513 var DefaultConfigTemplate = ` 514 cookbook_path [{{.CookbookPaths}}] 515 {{if .HasRolesPath}} 516 role_path "{{.RolesPath}}" 517 {{end}} 518 {{if .HasDataBagsPath}} 519 data_bag_path "{{.DataBagsPath}}" 520 {{end}} 521 {{if .HasEncryptedDataBagSecretPath}} 522 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 523 {{end}} 524 {{if .HasEnvironmentsPath}} 525 environment_path "{{.EnvironmentsPath}}" 526 environment "{{.ChefEnvironment}}" 527 {{end}} 528 `