github.com/supr/packer@v0.3.10-0.20131015195147-7b09e24ac3c1/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 EnvironmentsPath string `mapstructure:"environments_path"` 27 ExecuteCommand string `mapstructure:"execute_command"` 28 InstallCommand string `mapstructure:"install_command"` 29 RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"` 30 Json map[string]interface{} 31 PreventSudo bool `mapstructure:"prevent_sudo"` 32 RunList []string `mapstructure:"run_list"` 33 SkipInstall bool `mapstructure:"skip_install"` 34 StagingDir string `mapstructure:"staging_directory"` 35 36 tpl *packer.ConfigTemplate 37 } 38 39 type Provisioner struct { 40 config Config 41 } 42 43 type ConfigTemplate struct { 44 CookbookPaths string 45 DataBagsPath string 46 RolesPath string 47 EnvironmentsPath string 48 ChefEnvironment string 49 50 // Templates don't support boolean statements until Go 1.2. In the 51 // mean time, we do this. 52 // TODO(mitchellh): Remove when Go 1.2 is released 53 HasDataBagsPath bool 54 HasRolesPath bool 55 HasEnvironmentsPath bool 56 } 57 58 type ExecuteTemplate struct { 59 ConfigPath string 60 JsonPath string 61 Sudo bool 62 } 63 64 type InstallChefTemplate struct { 65 Sudo bool 66 } 67 68 func (p *Provisioner) Prepare(raws ...interface{}) error { 69 md, err := common.DecodeConfig(&p.config, raws...) 70 if err != nil { 71 return err 72 } 73 74 p.config.tpl, err = packer.NewConfigTemplate() 75 if err != nil { 76 return err 77 } 78 p.config.tpl.UserVars = p.config.PackerUserVars 79 80 if p.config.ExecuteCommand == "" { 81 p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}" 82 } 83 84 if p.config.InstallCommand == "" { 85 p.config.InstallCommand = "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo {{end}}bash" 86 } 87 88 if p.config.RunList == nil { 89 p.config.RunList = make([]string, 0) 90 } 91 92 if p.config.StagingDir == "" { 93 p.config.StagingDir = "/tmp/packer-chef-solo" 94 } 95 96 // Accumulate any errors 97 errs := common.CheckUnusedConfig(md) 98 99 templates := map[string]*string{ 100 "config_template": &p.config.ConfigTemplate, 101 "data_bags_path": &p.config.DataBagsPath, 102 "roles_path": &p.config.RolesPath, 103 "staging_dir": &p.config.StagingDir, 104 "environments_path": &p.config.EnvironmentsPath, 105 "chef_environment": &p.config.ChefEnvironment, 106 } 107 108 for n, ptr := range templates { 109 var err error 110 *ptr, err = p.config.tpl.Process(*ptr, nil) 111 if err != nil { 112 errs = packer.MultiErrorAppend( 113 errs, fmt.Errorf("Error processing %s: %s", n, err)) 114 } 115 } 116 117 sliceTemplates := map[string][]string{ 118 "cookbook_paths": p.config.CookbookPaths, 119 "remote_cookbook_paths": p.config.RemoteCookbookPaths, 120 "run_list": p.config.RunList, 121 } 122 123 for n, slice := range sliceTemplates { 124 for i, elem := range slice { 125 var err error 126 slice[i], err = p.config.tpl.Process(elem, nil) 127 if err != nil { 128 errs = packer.MultiErrorAppend( 129 errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err)) 130 } 131 } 132 } 133 134 validates := map[string]*string{ 135 "execute_command": &p.config.ExecuteCommand, 136 "install_command": &p.config.InstallCommand, 137 } 138 139 for n, ptr := range validates { 140 if err := p.config.tpl.Validate(*ptr); err != nil { 141 errs = packer.MultiErrorAppend( 142 errs, fmt.Errorf("Error parsing %s: %s", n, err)) 143 } 144 } 145 146 if p.config.ConfigTemplate != "" { 147 fi, err := os.Stat(p.config.ConfigTemplate) 148 if err != nil { 149 errs = packer.MultiErrorAppend( 150 errs, fmt.Errorf("Bad config template path: %s", err)) 151 } else if fi.IsDir() { 152 errs = packer.MultiErrorAppend( 153 errs, fmt.Errorf("Config template path must be a file: %s", err)) 154 } 155 } 156 157 for _, path := range p.config.CookbookPaths { 158 pFileInfo, err := os.Stat(path) 159 160 if err != nil || !pFileInfo.IsDir() { 161 errs = packer.MultiErrorAppend( 162 errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err)) 163 } 164 } 165 166 if p.config.RolesPath != "" { 167 pFileInfo, err := os.Stat(p.config.RolesPath) 168 169 if err != nil || !pFileInfo.IsDir() { 170 errs = packer.MultiErrorAppend( 171 errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err)) 172 } 173 } 174 175 if p.config.DataBagsPath != "" { 176 pFileInfo, err := os.Stat(p.config.DataBagsPath) 177 178 if err != nil || !pFileInfo.IsDir() { 179 errs = packer.MultiErrorAppend( 180 errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, err)) 181 } 182 } 183 184 if p.config.EnvironmentsPath != "" { 185 pFileInfo, err := os.Stat(p.config.EnvironmentsPath) 186 187 if err != nil || !pFileInfo.IsDir() { 188 errs = packer.MultiErrorAppend( 189 errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err)) 190 } 191 } 192 193 // Process the user variables within the JSON and set the JSON. 194 // Do this early so that we can validate and show errors. 195 p.config.Json, err = p.processJsonUserVars() 196 if err != nil { 197 errs = packer.MultiErrorAppend( 198 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 199 } 200 201 if errs != nil && len(errs.Errors) > 0 { 202 return errs 203 } 204 205 return nil 206 } 207 208 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 209 if !p.config.SkipInstall { 210 if err := p.installChef(ui, comm); err != nil { 211 return fmt.Errorf("Error installing Chef: %s", err) 212 } 213 } 214 215 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 216 return fmt.Errorf("Error creating staging directory: %s", err) 217 } 218 219 cookbookPaths := make([]string, 0, len(p.config.CookbookPaths)) 220 for i, path := range p.config.CookbookPaths { 221 targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i) 222 if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil { 223 return fmt.Errorf("Error uploading cookbooks: %s", err) 224 } 225 226 cookbookPaths = append(cookbookPaths, targetPath) 227 } 228 229 rolesPath := "" 230 if p.config.RolesPath != "" { 231 rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir) 232 if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil { 233 return fmt.Errorf("Error uploading roles: %s", err) 234 } 235 } 236 237 dataBagsPath := "" 238 if p.config.DataBagsPath != "" { 239 dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir) 240 if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil { 241 return fmt.Errorf("Error uploading data bags: %s", err) 242 } 243 } 244 245 environmentsPath := "" 246 if p.config.EnvironmentsPath != "" { 247 environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir) 248 if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil { 249 return fmt.Errorf("Error uploading environments: %s", err) 250 } 251 } 252 253 configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, environmentsPath, p.config.ChefEnvironment) 254 if err != nil { 255 return fmt.Errorf("Error creating Chef config file: %s", err) 256 } 257 258 jsonPath, err := p.createJson(ui, comm) 259 if err != nil { 260 return fmt.Errorf("Error creating JSON attributes: %s", err) 261 } 262 263 if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil { 264 return fmt.Errorf("Error executing Chef: %s", err) 265 } 266 267 return nil 268 } 269 270 func (p *Provisioner) Cancel() { 271 // Just hard quit. It isn't a big deal if what we're doing keeps 272 // running on the other side. 273 os.Exit(0) 274 } 275 276 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 277 if err := p.createDir(ui, comm, dst); err != nil { 278 return err 279 } 280 281 // Make sure there is a trailing "/" so that the directory isn't 282 // created on the other side. 283 if src[len(src)-1] != '/' { 284 src = src + "/" 285 } 286 287 return comm.UploadDir(dst, src, nil) 288 } 289 290 func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, environmentsPath string, chefEnvironment string) (string, error) { 291 ui.Message("Creating configuration file 'solo.rb'") 292 293 cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks)) 294 for i, path := range p.config.RemoteCookbookPaths { 295 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 296 } 297 298 for i, path := range localCookbooks { 299 i = len(p.config.RemoteCookbookPaths) + i 300 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 301 } 302 303 // Read the template 304 tpl := DefaultConfigTemplate 305 if p.config.ConfigTemplate != "" { 306 f, err := os.Open(p.config.ConfigTemplate) 307 if err != nil { 308 return "", err 309 } 310 defer f.Close() 311 312 tplBytes, err := ioutil.ReadAll(f) 313 if err != nil { 314 return "", err 315 } 316 317 tpl = string(tplBytes) 318 } 319 320 configString, err := p.config.tpl.Process(tpl, &ConfigTemplate{ 321 CookbookPaths: strings.Join(cookbook_paths, ","), 322 RolesPath: rolesPath, 323 DataBagsPath: dataBagsPath, 324 EnvironmentsPath: environmentsPath, 325 HasRolesPath: rolesPath != "", 326 HasDataBagsPath: dataBagsPath != "", 327 HasEnvironmentsPath: environmentsPath != "", 328 ChefEnvironment: chefEnvironment, 329 }) 330 if err != nil { 331 return "", err 332 } 333 334 remotePath := filepath.Join(p.config.StagingDir, "solo.rb") 335 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString))); err != nil { 336 return "", err 337 } 338 339 return remotePath, nil 340 } 341 342 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 343 ui.Message("Creating JSON attribute file") 344 345 jsonData := make(map[string]interface{}) 346 347 // Copy the configured JSON 348 for k, v := range p.config.Json { 349 jsonData[k] = v 350 } 351 352 // Set the run list if it was specified 353 if len(p.config.RunList) > 0 { 354 jsonData["run_list"] = p.config.RunList 355 } 356 357 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 358 if err != nil { 359 return "", err 360 } 361 362 // Upload the bytes 363 remotePath := filepath.Join(p.config.StagingDir, "node.json") 364 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes)); err != nil { 365 return "", err 366 } 367 368 return remotePath, nil 369 } 370 371 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 372 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 373 cmd := &packer.RemoteCmd{ 374 Command: fmt.Sprintf("mkdir -p '%s'", dir), 375 } 376 377 if err := cmd.StartWithUi(comm, ui); err != nil { 378 return err 379 } 380 381 if cmd.ExitStatus != 0 { 382 return fmt.Errorf("Non-zero exit status.") 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) processJsonUserVars() (map[string]interface{}, error) { 439 jsonBytes, err := json.Marshal(p.config.Json) 440 if err != nil { 441 // This really shouldn't happen since we literally just unmarshalled 442 panic(err) 443 } 444 445 // Copy the user variables so that we can restore them later, and 446 // make sure we make the quotes JSON-friendly in the user variables. 447 originalUserVars := make(map[string]string) 448 for k, v := range p.config.tpl.UserVars { 449 originalUserVars[k] = v 450 } 451 452 // Make sure we reset them no matter what 453 defer func() { 454 p.config.tpl.UserVars = originalUserVars 455 }() 456 457 // Make the current user variables JSON string safe. 458 for k, v := range p.config.tpl.UserVars { 459 v = strings.Replace(v, `\`, `\\`, -1) 460 v = strings.Replace(v, `"`, `\"`, -1) 461 p.config.tpl.UserVars[k] = v 462 } 463 464 // Process the bytes with the template processor 465 jsonBytesProcessed, err := p.config.tpl.Process(string(jsonBytes), nil) 466 if err != nil { 467 return nil, err 468 } 469 470 var result map[string]interface{} 471 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 472 return nil, err 473 } 474 475 return result, nil 476 } 477 478 var DefaultConfigTemplate = ` 479 cookbook_path [{{.CookbookPaths}}] 480 {{if .HasRolesPath}} 481 role_path "{{.RolesPath}}" 482 {{end}} 483 {{if .HasDataBagsPath}} 484 data_bag_path "{{.DataBagsPath}}" 485 {{end}} 486 {{if .HasEnvironmentsPath}} 487 environments_path "{{.EnvironmentsPath}}" 488 chef_environment "{{.ChefEnvironment}}" 489 {{end}} 490 `