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  `