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