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  `