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