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