github.com/dacamp/packer@v0.10.2/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/provisioner"
    19  	"github.com/mitchellh/packer/template/interpolate"
    20  )
    21  
    22  type guestOSTypeConfig struct {
    23  	executeCommand string
    24  	installCommand string
    25  	stagingDir     string
    26  }
    27  
    28  var guestOSTypeConfigs = map[string]guestOSTypeConfig{
    29  	provisioner.UnixOSType: guestOSTypeConfig{
    30  		executeCommand: "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}",
    31  		installCommand: "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash",
    32  		stagingDir:     "/tmp/packer-chef-client",
    33  	},
    34  	provisioner.WindowsOSType: guestOSTypeConfig{
    35  		executeCommand: "c:/opscode/chef/bin/chef-solo.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}",
    36  		installCommand: "powershell.exe -Command \"(New-Object System.Net.WebClient).DownloadFile('http://chef.io/chef/install.msi', 'C:\\Windows\\Temp\\chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait\"",
    37  		stagingDir:     "C:/Windows/Temp/packer-chef-client",
    38  	},
    39  }
    40  
    41  type Config struct {
    42  	common.PackerConfig `mapstructure:",squash"`
    43  
    44  	ChefEnvironment            string   `mapstructure:"chef_environment"`
    45  	ConfigTemplate             string   `mapstructure:"config_template"`
    46  	CookbookPaths              []string `mapstructure:"cookbook_paths"`
    47  	RolesPath                  string   `mapstructure:"roles_path"`
    48  	DataBagsPath               string   `mapstructure:"data_bags_path"`
    49  	EncryptedDataBagSecretPath string   `mapstructure:"encrypted_data_bag_secret_path"`
    50  	EnvironmentsPath           string   `mapstructure:"environments_path"`
    51  	ExecuteCommand             string   `mapstructure:"execute_command"`
    52  	InstallCommand             string   `mapstructure:"install_command"`
    53  	RemoteCookbookPaths        []string `mapstructure:"remote_cookbook_paths"`
    54  	Json                       map[string]interface{}
    55  	PreventSudo                bool     `mapstructure:"prevent_sudo"`
    56  	RunList                    []string `mapstructure:"run_list"`
    57  	SkipInstall                bool     `mapstructure:"skip_install"`
    58  	StagingDir                 string   `mapstructure:"staging_directory"`
    59  	GuestOSType                string   `mapstructure:"guest_os_type"`
    60  
    61  	ctx interpolate.Context
    62  }
    63  
    64  type Provisioner struct {
    65  	config            Config
    66  	guestOSTypeConfig guestOSTypeConfig
    67  	guestCommands     *provisioner.GuestCommands
    68  }
    69  
    70  type ConfigTemplate struct {
    71  	CookbookPaths              string
    72  	DataBagsPath               string
    73  	EncryptedDataBagSecretPath string
    74  	RolesPath                  string
    75  	EnvironmentsPath           string
    76  	ChefEnvironment            string
    77  
    78  	// Templates don't support boolean statements until Go 1.2. In the
    79  	// mean time, we do this.
    80  	// TODO(mitchellh): Remove when Go 1.2 is released
    81  	HasDataBagsPath               bool
    82  	HasEncryptedDataBagSecretPath bool
    83  	HasRolesPath                  bool
    84  	HasEnvironmentsPath           bool
    85  }
    86  
    87  type ExecuteTemplate struct {
    88  	ConfigPath string
    89  	JsonPath   string
    90  	Sudo       bool
    91  }
    92  
    93  type InstallChefTemplate struct {
    94  	Sudo bool
    95  }
    96  
    97  func (p *Provisioner) Prepare(raws ...interface{}) error {
    98  	err := config.Decode(&p.config, &config.DecodeOpts{
    99  		Interpolate:        true,
   100  		InterpolateContext: &p.config.ctx,
   101  		InterpolateFilter: &interpolate.RenderFilter{
   102  			Exclude: []string{
   103  				"execute_command",
   104  				"install_command",
   105  			},
   106  		},
   107  	}, raws...)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if p.config.GuestOSType == "" {
   113  		p.config.GuestOSType = provisioner.DefaultOSType
   114  	}
   115  	p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
   116  
   117  	var ok bool
   118  	p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
   119  	if !ok {
   120  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   121  	}
   122  
   123  	p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo)
   124  	if err != nil {
   125  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   126  	}
   127  
   128  	if p.config.ExecuteCommand == "" {
   129  		p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
   130  	}
   131  
   132  	if p.config.InstallCommand == "" {
   133  		p.config.InstallCommand = p.guestOSTypeConfig.installCommand
   134  	}
   135  
   136  	if p.config.RunList == nil {
   137  		p.config.RunList = make([]string, 0)
   138  	}
   139  
   140  	if p.config.StagingDir == "" {
   141  		p.config.StagingDir = p.guestOSTypeConfig.stagingDir
   142  	}
   143  
   144  	var errs *packer.MultiError
   145  	if p.config.ConfigTemplate != "" {
   146  		fi, err := os.Stat(p.config.ConfigTemplate)
   147  		if err != nil {
   148  			errs = packer.MultiErrorAppend(
   149  				errs, fmt.Errorf("Bad config template path: %s", err))
   150  		} else if fi.IsDir() {
   151  			errs = packer.MultiErrorAppend(
   152  				errs, fmt.Errorf("Config template path must be a file: %s", err))
   153  		}
   154  	}
   155  
   156  	for _, path := range p.config.CookbookPaths {
   157  		pFileInfo, err := os.Stat(path)
   158  
   159  		if err != nil || !pFileInfo.IsDir() {
   160  			errs = packer.MultiErrorAppend(
   161  				errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err))
   162  		}
   163  	}
   164  
   165  	if p.config.RolesPath != "" {
   166  		pFileInfo, err := os.Stat(p.config.RolesPath)
   167  
   168  		if err != nil || !pFileInfo.IsDir() {
   169  			errs = packer.MultiErrorAppend(
   170  				errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err))
   171  		}
   172  	}
   173  
   174  	if p.config.DataBagsPath != "" {
   175  		pFileInfo, err := os.Stat(p.config.DataBagsPath)
   176  
   177  		if err != nil || !pFileInfo.IsDir() {
   178  			errs = packer.MultiErrorAppend(
   179  				errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, err))
   180  		}
   181  	}
   182  
   183  	if p.config.EncryptedDataBagSecretPath != "" {
   184  		pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath)
   185  
   186  		if err != nil || pFileInfo.IsDir() {
   187  			errs = packer.MultiErrorAppend(
   188  				errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err))
   189  		}
   190  	}
   191  
   192  	if p.config.EnvironmentsPath != "" {
   193  		pFileInfo, err := os.Stat(p.config.EnvironmentsPath)
   194  
   195  		if err != nil || !pFileInfo.IsDir() {
   196  			errs = packer.MultiErrorAppend(
   197  				errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err))
   198  		}
   199  	}
   200  
   201  	jsonValid := true
   202  	for k, v := range p.config.Json {
   203  		p.config.Json[k], err = p.deepJsonFix(k, v)
   204  		if err != nil {
   205  			errs = packer.MultiErrorAppend(
   206  				errs, fmt.Errorf("Error processing JSON: %s", err))
   207  			jsonValid = false
   208  		}
   209  	}
   210  
   211  	if jsonValid {
   212  		// Process the user variables within the JSON and set the JSON.
   213  		// Do this early so that we can validate and show errors.
   214  		p.config.Json, err = p.processJsonUserVars()
   215  		if err != nil {
   216  			errs = packer.MultiErrorAppend(
   217  				errs, fmt.Errorf("Error processing user variables in JSON: %s", err))
   218  		}
   219  	}
   220  
   221  	if errs != nil && len(errs.Errors) > 0 {
   222  		return errs
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   229  	ui.Say("Provisioning with chef-solo")
   230  
   231  	if !p.config.SkipInstall {
   232  		if err := p.installChef(ui, comm); err != nil {
   233  			return fmt.Errorf("Error installing Chef: %s", err)
   234  		}
   235  	}
   236  
   237  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   238  		return fmt.Errorf("Error creating staging directory: %s", err)
   239  	}
   240  
   241  	cookbookPaths := make([]string, 0, len(p.config.CookbookPaths))
   242  	for i, path := range p.config.CookbookPaths {
   243  		targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i)
   244  		if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
   245  			return fmt.Errorf("Error uploading cookbooks: %s", err)
   246  		}
   247  
   248  		cookbookPaths = append(cookbookPaths, targetPath)
   249  	}
   250  
   251  	rolesPath := ""
   252  	if p.config.RolesPath != "" {
   253  		rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir)
   254  		if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil {
   255  			return fmt.Errorf("Error uploading roles: %s", err)
   256  		}
   257  	}
   258  
   259  	dataBagsPath := ""
   260  	if p.config.DataBagsPath != "" {
   261  		dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir)
   262  		if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil {
   263  			return fmt.Errorf("Error uploading data bags: %s", err)
   264  		}
   265  	}
   266  
   267  	encryptedDataBagSecretPath := ""
   268  	if p.config.EncryptedDataBagSecretPath != "" {
   269  		encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir)
   270  		if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil {
   271  			return fmt.Errorf("Error uploading encrypted data bag secret: %s", err)
   272  		}
   273  	}
   274  
   275  	environmentsPath := ""
   276  	if p.config.EnvironmentsPath != "" {
   277  		environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir)
   278  		if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil {
   279  			return fmt.Errorf("Error uploading environments: %s", err)
   280  		}
   281  	}
   282  
   283  	configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, encryptedDataBagSecretPath, environmentsPath, p.config.ChefEnvironment)
   284  	if err != nil {
   285  		return fmt.Errorf("Error creating Chef config file: %s", err)
   286  	}
   287  
   288  	jsonPath, err := p.createJson(ui, comm)
   289  	if err != nil {
   290  		return fmt.Errorf("Error creating JSON attributes: %s", err)
   291  	}
   292  
   293  	if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil {
   294  		return fmt.Errorf("Error executing Chef: %s", err)
   295  	}
   296  
   297  	return nil
   298  }
   299  
   300  func (p *Provisioner) Cancel() {
   301  	// Just hard quit. It isn't a big deal if what we're doing keeps
   302  	// running on the other side.
   303  	os.Exit(0)
   304  }
   305  
   306  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   307  	if err := p.createDir(ui, comm, dst); err != nil {
   308  		return err
   309  	}
   310  
   311  	// Make sure there is a trailing "/" so that the directory isn't
   312  	// created on the other side.
   313  	if src[len(src)-1] != '/' {
   314  		src = src + "/"
   315  	}
   316  
   317  	return comm.UploadDir(dst, src, nil)
   318  }
   319  
   320  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   321  	f, err := os.Open(src)
   322  	if err != nil {
   323  		return err
   324  	}
   325  	defer f.Close()
   326  
   327  	return comm.Upload(dst, f, nil)
   328  }
   329  
   330  func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, encryptedDataBagSecretPath string, environmentsPath string, chefEnvironment string) (string, error) {
   331  	ui.Message("Creating configuration file 'solo.rb'")
   332  
   333  	cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks))
   334  	for i, path := range p.config.RemoteCookbookPaths {
   335  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   336  	}
   337  
   338  	for i, path := range localCookbooks {
   339  		i = len(p.config.RemoteCookbookPaths) + i
   340  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   341  	}
   342  
   343  	// Read the template
   344  	tpl := DefaultConfigTemplate
   345  	if p.config.ConfigTemplate != "" {
   346  		f, err := os.Open(p.config.ConfigTemplate)
   347  		if err != nil {
   348  			return "", err
   349  		}
   350  		defer f.Close()
   351  
   352  		tplBytes, err := ioutil.ReadAll(f)
   353  		if err != nil {
   354  			return "", err
   355  		}
   356  
   357  		tpl = string(tplBytes)
   358  	}
   359  
   360  	p.config.ctx.Data = &ConfigTemplate{
   361  		CookbookPaths:                 strings.Join(cookbook_paths, ","),
   362  		RolesPath:                     rolesPath,
   363  		DataBagsPath:                  dataBagsPath,
   364  		EncryptedDataBagSecretPath:    encryptedDataBagSecretPath,
   365  		EnvironmentsPath:              environmentsPath,
   366  		HasRolesPath:                  rolesPath != "",
   367  		HasDataBagsPath:               dataBagsPath != "",
   368  		HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "",
   369  		HasEnvironmentsPath:           environmentsPath != "",
   370  		ChefEnvironment:               chefEnvironment,
   371  	}
   372  	configString, err := interpolate.Render(tpl, &p.config.ctx)
   373  	if err != nil {
   374  		return "", err
   375  	}
   376  
   377  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "solo.rb"))
   378  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil {
   379  		return "", err
   380  	}
   381  
   382  	return remotePath, nil
   383  }
   384  
   385  func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
   386  	ui.Message("Creating JSON attribute file")
   387  
   388  	jsonData := make(map[string]interface{})
   389  
   390  	// Copy the configured JSON
   391  	for k, v := range p.config.Json {
   392  		jsonData[k] = v
   393  	}
   394  
   395  	// Set the run list if it was specified
   396  	if len(p.config.RunList) > 0 {
   397  		jsonData["run_list"] = p.config.RunList
   398  	}
   399  
   400  	jsonBytes, err := json.MarshalIndent(jsonData, "", "  ")
   401  	if err != nil {
   402  		return "", err
   403  	}
   404  
   405  	// Upload the bytes
   406  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "node.json"))
   407  	if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil {
   408  		return "", err
   409  	}
   410  
   411  	return remotePath, nil
   412  }
   413  
   414  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   415  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   416  
   417  	cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
   418  	if err := cmd.StartWithUi(comm, ui); err != nil {
   419  		return err
   420  	}
   421  	if cmd.ExitStatus != 0 {
   422  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   423  	}
   424  
   425  	// Chmod the directory to 0777 just so that we can access it as our user
   426  	cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
   427  	if err := cmd.StartWithUi(comm, ui); err != nil {
   428  		return err
   429  	}
   430  	if cmd.ExitStatus != 0 {
   431  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
   438  	p.config.ctx.Data = &ExecuteTemplate{
   439  		ConfigPath: config,
   440  		JsonPath:   json,
   441  		Sudo:       !p.config.PreventSudo,
   442  	}
   443  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   444  	if err != nil {
   445  		return err
   446  	}
   447  
   448  	ui.Message(fmt.Sprintf("Executing Chef: %s", command))
   449  
   450  	cmd := &packer.RemoteCmd{
   451  		Command: command,
   452  	}
   453  
   454  	if err := cmd.StartWithUi(comm, ui); err != nil {
   455  		return err
   456  	}
   457  
   458  	if cmd.ExitStatus != 0 {
   459  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   460  	}
   461  
   462  	return nil
   463  }
   464  
   465  func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error {
   466  	ui.Message("Installing Chef...")
   467  
   468  	p.config.ctx.Data = &InstallChefTemplate{
   469  		Sudo: !p.config.PreventSudo,
   470  	}
   471  	command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx)
   472  	if err != nil {
   473  		return err
   474  	}
   475  
   476  	cmd := &packer.RemoteCmd{Command: command}
   477  	if err := cmd.StartWithUi(comm, ui); err != nil {
   478  		return err
   479  	}
   480  
   481  	if cmd.ExitStatus != 0 {
   482  		return fmt.Errorf(
   483  			"Install script exited with non-zero exit status %d", cmd.ExitStatus)
   484  	}
   485  
   486  	return nil
   487  }
   488  
   489  func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) {
   490  	if current == nil {
   491  		return nil, nil
   492  	}
   493  
   494  	switch c := current.(type) {
   495  	case []interface{}:
   496  		val := make([]interface{}, len(c))
   497  		for i, v := range c {
   498  			var err error
   499  			val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v)
   500  			if err != nil {
   501  				return nil, err
   502  			}
   503  		}
   504  
   505  		return val, nil
   506  	case []uint8:
   507  		return string(c), nil
   508  	case map[interface{}]interface{}:
   509  		val := make(map[string]interface{})
   510  		for k, v := range c {
   511  			ks, ok := k.(string)
   512  			if !ok {
   513  				return nil, fmt.Errorf("%s: key is not string", key)
   514  			}
   515  
   516  			var err error
   517  			val[ks], err = p.deepJsonFix(
   518  				fmt.Sprintf("%s.%s", key, ks), v)
   519  			if err != nil {
   520  				return nil, err
   521  			}
   522  		}
   523  
   524  		return val, nil
   525  	default:
   526  		return current, nil
   527  	}
   528  }
   529  
   530  func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
   531  	jsonBytes, err := json.Marshal(p.config.Json)
   532  	if err != nil {
   533  		// This really shouldn't happen since we literally just unmarshalled
   534  		panic(err)
   535  	}
   536  
   537  	// Copy the user variables so that we can restore them later, and
   538  	// make sure we make the quotes JSON-friendly in the user variables.
   539  	originalUserVars := make(map[string]string)
   540  	for k, v := range p.config.ctx.UserVariables {
   541  		originalUserVars[k] = v
   542  	}
   543  
   544  	// Make sure we reset them no matter what
   545  	defer func() {
   546  		p.config.ctx.UserVariables = originalUserVars
   547  	}()
   548  
   549  	// Make the current user variables JSON string safe.
   550  	for k, v := range p.config.ctx.UserVariables {
   551  		v = strings.Replace(v, `\`, `\\`, -1)
   552  		v = strings.Replace(v, `"`, `\"`, -1)
   553  		p.config.ctx.UserVariables[k] = v
   554  	}
   555  
   556  	// Process the bytes with the template processor
   557  	p.config.ctx.Data = nil
   558  	jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx)
   559  	if err != nil {
   560  		return nil, err
   561  	}
   562  
   563  	var result map[string]interface{}
   564  	if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil {
   565  		return nil, err
   566  	}
   567  
   568  	return result, nil
   569  }
   570  
   571  var DefaultConfigTemplate = `
   572  cookbook_path 	[{{.CookbookPaths}}]
   573  {{if .HasRolesPath}}
   574  role_path		"{{.RolesPath}}"
   575  {{end}}
   576  {{if .HasDataBagsPath}}
   577  data_bag_path	"{{.DataBagsPath}}"
   578  {{end}}
   579  {{if .HasEncryptedDataBagSecretPath}}
   580  encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}"
   581  {{end}}
   582  {{if .HasEnvironmentsPath}}
   583  environment_path "{{.EnvironmentsPath}}"
   584  environment "{{.ChefEnvironment}}"
   585  {{end}}
   586  `