github.com/ttysteale/packer@v0.8.2-0.20150708160520-e5f8ea386ed8/provisioner/chef-client/provisioner.go (about)

     1  // This package implements a provisioner for Packer that uses
     2  // Chef to provision the remote machine, specifically with chef-client (that is,
     3  // with a Chef server).
     4  package chefclient
     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/common/uuid"
    17  	"github.com/mitchellh/packer/helper/config"
    18  	"github.com/mitchellh/packer/packer"
    19  	"github.com/mitchellh/packer/template/interpolate"
    20  )
    21  
    22  type Config struct {
    23  	common.PackerConfig `mapstructure:",squash"`
    24  
    25  	ChefEnvironment      string `mapstructure:"chef_environment"`
    26  	SslVerifyMode        string `mapstructure:"ssl_verify_mode"`
    27  	ConfigTemplate       string `mapstructure:"config_template"`
    28  	ExecuteCommand       string `mapstructure:"execute_command"`
    29  	InstallCommand       string `mapstructure:"install_command"`
    30  	Json                 map[string]interface{}
    31  	NodeName             string   `mapstructure:"node_name"`
    32  	PreventSudo          bool     `mapstructure:"prevent_sudo"`
    33  	RunList              []string `mapstructure:"run_list"`
    34  	ServerUrl            string   `mapstructure:"server_url"`
    35  	SkipCleanClient      bool     `mapstructure:"skip_clean_client"`
    36  	SkipCleanNode        bool     `mapstructure:"skip_clean_node"`
    37  	SkipInstall          bool     `mapstructure:"skip_install"`
    38  	StagingDir           string   `mapstructure:"staging_directory"`
    39  	ClientKey            string   `mapstructure:"client_key"`
    40  	ValidationKeyPath    string   `mapstructure:"validation_key_path"`
    41  	ValidationClientName string   `mapstructure:"validation_client_name"`
    42  
    43  	ctx interpolate.Context
    44  }
    45  
    46  type Provisioner struct {
    47  	config Config
    48  }
    49  
    50  type ConfigTemplate struct {
    51  	NodeName             string
    52  	ServerUrl            string
    53  	ClientKey            string
    54  	ValidationKeyPath    string
    55  	ValidationClientName string
    56  	ChefEnvironment      string
    57  	SslVerifyMode        string
    58  }
    59  
    60  type ExecuteTemplate struct {
    61  	ConfigPath string
    62  	JsonPath   string
    63  	Sudo       bool
    64  }
    65  
    66  type InstallChefTemplate struct {
    67  	Sudo bool
    68  }
    69  
    70  func (p *Provisioner) Prepare(raws ...interface{}) error {
    71  	err := config.Decode(&p.config, &config.DecodeOpts{
    72  		Interpolate:        true,
    73  		InterpolateContext: &p.config.ctx,
    74  		InterpolateFilter: &interpolate.RenderFilter{
    75  			Exclude: []string{
    76  				"execute_command",
    77  				"install_command",
    78  			},
    79  		},
    80  	}, raws...)
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	if p.config.ExecuteCommand == "" {
    86  		p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-client " +
    87  			"--no-color -c {{.ConfigPath}} -j {{.JsonPath}}"
    88  	}
    89  
    90  	if p.config.InstallCommand == "" {
    91  		p.config.InstallCommand = "curl -L " +
    92  			"https://www.opscode.com/chef/install.sh | " +
    93  			"{{if .Sudo}}sudo {{end}}bash"
    94  	}
    95  
    96  	if p.config.RunList == nil {
    97  		p.config.RunList = make([]string, 0)
    98  	}
    99  
   100  	if p.config.StagingDir == "" {
   101  		p.config.StagingDir = "/tmp/packer-chef-client"
   102  	}
   103  
   104  	var errs *packer.MultiError
   105  	if p.config.ConfigTemplate != "" {
   106  		fi, err := os.Stat(p.config.ConfigTemplate)
   107  		if err != nil {
   108  			errs = packer.MultiErrorAppend(
   109  				errs, fmt.Errorf("Bad config template path: %s", err))
   110  		} else if fi.IsDir() {
   111  			errs = packer.MultiErrorAppend(
   112  				errs, fmt.Errorf("Config template path must be a file: %s", err))
   113  		}
   114  	}
   115  
   116  	if p.config.ServerUrl == "" {
   117  		errs = packer.MultiErrorAppend(
   118  			errs, fmt.Errorf("server_url must be set"))
   119  	}
   120  
   121  	jsonValid := true
   122  	for k, v := range p.config.Json {
   123  		p.config.Json[k], err = p.deepJsonFix(k, v)
   124  		if err != nil {
   125  			errs = packer.MultiErrorAppend(
   126  				errs, fmt.Errorf("Error processing JSON: %s", err))
   127  			jsonValid = false
   128  		}
   129  	}
   130  
   131  	if jsonValid {
   132  		// Process the user variables within the JSON and set the JSON.
   133  		// Do this early so that we can validate and show errors.
   134  		p.config.Json, err = p.processJsonUserVars()
   135  		if err != nil {
   136  			errs = packer.MultiErrorAppend(
   137  				errs, fmt.Errorf("Error processing user variables in JSON: %s", err))
   138  		}
   139  	}
   140  
   141  	if errs != nil && len(errs.Errors) > 0 {
   142  		return errs
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   149  
   150  	nodeName := p.config.NodeName
   151  	if nodeName == "" {
   152  		nodeName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
   153  	}
   154  	remoteValidationKeyPath := ""
   155  	serverUrl := p.config.ServerUrl
   156  
   157  	if !p.config.SkipInstall {
   158  		if err := p.installChef(ui, comm); err != nil {
   159  			return fmt.Errorf("Error installing Chef: %s", err)
   160  		}
   161  	}
   162  
   163  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   164  		return fmt.Errorf("Error creating staging directory: %s", err)
   165  	}
   166  
   167  	if p.config.ClientKey == "" {
   168  		p.config.ClientKey = fmt.Sprintf("%s/client.pem", p.config.StagingDir)
   169  	}
   170  
   171  	if p.config.ValidationKeyPath != "" {
   172  		remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir)
   173  		if err := p.copyValidationKey(ui, comm, remoteValidationKeyPath); err != nil {
   174  			return fmt.Errorf("Error copying validation key: %s", err)
   175  		}
   176  	}
   177  
   178  	configPath, err := p.createConfig(
   179  		ui, comm, nodeName, serverUrl, p.config.ClientKey, remoteValidationKeyPath, p.config.ValidationClientName, p.config.ChefEnvironment, p.config.SslVerifyMode)
   180  	if err != nil {
   181  		return fmt.Errorf("Error creating Chef config file: %s", err)
   182  	}
   183  
   184  	jsonPath, err := p.createJson(ui, comm)
   185  	if err != nil {
   186  		return fmt.Errorf("Error creating JSON attributes: %s", err)
   187  	}
   188  
   189  	err = p.executeChef(ui, comm, configPath, jsonPath)
   190  
   191  	knifeConfigPath, err2 := p.createKnifeConfig(
   192  		ui, comm, nodeName, serverUrl, p.config.ClientKey, p.config.SslVerifyMode)
   193  	if err2 != nil {
   194  		return fmt.Errorf("Error creating knife config on node: %s", err2)
   195  	}
   196  	if !p.config.SkipCleanNode {
   197  		if err2 := p.cleanNode(ui, comm, nodeName, knifeConfigPath); err2 != nil {
   198  			return fmt.Errorf("Error cleaning up chef node: %s", err2)
   199  		}
   200  	}
   201  
   202  	if !p.config.SkipCleanClient {
   203  		if err2 := p.cleanClient(ui, comm, nodeName, knifeConfigPath); err2 != nil {
   204  			return fmt.Errorf("Error cleaning up chef client: %s", err2)
   205  		}
   206  	}
   207  
   208  	if err != nil {
   209  		return fmt.Errorf("Error executing Chef: %s", err)
   210  	}
   211  
   212  	if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
   213  		return fmt.Errorf("Error removing /etc/chef directory: %s", err)
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (p *Provisioner) Cancel() {
   220  	// Just hard quit. It isn't a big deal if what we're doing keeps
   221  	// running on the other side.
   222  	os.Exit(0)
   223  }
   224  
   225  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   226  	if err := p.createDir(ui, comm, dst); err != nil {
   227  		return err
   228  	}
   229  
   230  	// Make sure there is a trailing "/" so that the directory isn't
   231  	// created on the other side.
   232  	if src[len(src)-1] != '/' {
   233  		src = src + "/"
   234  	}
   235  
   236  	return comm.UploadDir(dst, src, nil)
   237  }
   238  
   239  func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, remoteKeyPath string, validationClientName string, chefEnvironment string, sslVerifyMode string) (string, error) {
   240  	ui.Message("Creating configuration file 'client.rb'")
   241  
   242  	// Read the template
   243  	tpl := DefaultConfigTemplate
   244  	if p.config.ConfigTemplate != "" {
   245  		f, err := os.Open(p.config.ConfigTemplate)
   246  		if err != nil {
   247  			return "", err
   248  		}
   249  		defer f.Close()
   250  
   251  		tplBytes, err := ioutil.ReadAll(f)
   252  		if err != nil {
   253  			return "", err
   254  		}
   255  
   256  		tpl = string(tplBytes)
   257  	}
   258  
   259  	ctx := p.config.ctx
   260  	ctx.Data = &ConfigTemplate{
   261  		NodeName:             nodeName,
   262  		ServerUrl:            serverUrl,
   263  		ClientKey:            clientKey,
   264  		ValidationKeyPath:    remoteKeyPath,
   265  		ValidationClientName: validationClientName,
   266  		ChefEnvironment:      chefEnvironment,
   267  		SslVerifyMode:        sslVerifyMode,
   268  	}
   269  	configString, err := interpolate.Render(tpl, &ctx)
   270  	if err != nil {
   271  		return "", err
   272  	}
   273  
   274  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb"))
   275  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil {
   276  		return "", err
   277  	}
   278  
   279  	return remotePath, nil
   280  }
   281  
   282  func (p *Provisioner) createKnifeConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, sslVerifyMode string) (string, error) {
   283  	ui.Message("Creating configuration file 'knife.rb'")
   284  
   285  	// Read the template
   286  	tpl := DefaultKnifeTemplate
   287  
   288  	ctx := p.config.ctx
   289  	ctx.Data = &ConfigTemplate{
   290  		NodeName:             nodeName,
   291  		ServerUrl:            serverUrl,
   292  		ClientKey:            clientKey,
   293  		SslVerifyMode:        sslVerifyMode,
   294  	}
   295  	configString, err := interpolate.Render(tpl, &ctx)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  
   300  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "knife.rb"))
   301  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil {
   302  		return "", err
   303  	}
   304  
   305  	return remotePath, nil
   306  }
   307  
   308  func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
   309  	ui.Message("Creating JSON attribute file")
   310  
   311  	jsonData := make(map[string]interface{})
   312  
   313  	// Copy the configured JSON
   314  	for k, v := range p.config.Json {
   315  		jsonData[k] = v
   316  	}
   317  
   318  	// Set the run list if it was specified
   319  	if len(p.config.RunList) > 0 {
   320  		jsonData["run_list"] = p.config.RunList
   321  	}
   322  
   323  	jsonBytes, err := json.MarshalIndent(jsonData, "", "  ")
   324  	if err != nil {
   325  		return "", err
   326  	}
   327  
   328  	// Upload the bytes
   329  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json"))
   330  	if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil {
   331  		return "", err
   332  	}
   333  
   334  	return remotePath, nil
   335  }
   336  
   337  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   338  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   339  
   340  	mkdirCmd := fmt.Sprintf("mkdir -p '%s'", dir)
   341  	if !p.config.PreventSudo {
   342  		mkdirCmd = "sudo " + mkdirCmd
   343  	}
   344  
   345  	cmd := &packer.RemoteCmd{Command: mkdirCmd}
   346  	if err := cmd.StartWithUi(comm, ui); err != nil {
   347  		return err
   348  	}
   349  	if cmd.ExitStatus != 0 {
   350  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   351  	}
   352  
   353  	// Chmod the directory to 0777 just so that we can access it as our user
   354  	mkdirCmd = fmt.Sprintf("chmod 0777 '%s'", dir)
   355  	if !p.config.PreventSudo {
   356  		mkdirCmd = "sudo " + mkdirCmd
   357  	}
   358  	cmd = &packer.RemoteCmd{Command: mkdirCmd}
   359  	if err := cmd.StartWithUi(comm, ui); err != nil {
   360  		return err
   361  	}
   362  	if cmd.ExitStatus != 0 {
   363  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   364  	}
   365  
   366  	return nil
   367  }
   368  
   369  func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error {
   370  	ui.Say("Cleaning up chef node...")
   371  	args := []string{"node", "delete", node}
   372  	if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil {
   373  		return fmt.Errorf("Failed to cleanup node: %s", err)
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error {
   380  	ui.Say("Cleaning up chef client...")
   381  	args := []string{"client", "delete", node}
   382  	if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil {
   383  		return fmt.Errorf("Failed to cleanup client: %s", err)
   384  	}
   385  
   386  	return nil
   387  }
   388  
   389  func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string, args []string) error {
   390  	flags := []string{
   391  		"-y",
   392  		"-c", knifeConfigPath,
   393  	}
   394  
   395  	cmdText := fmt.Sprintf(
   396  		"knife %s %s", strings.Join(args, " "), strings.Join(flags, " "))
   397  	if !p.config.PreventSudo {
   398  		cmdText = "sudo " + cmdText
   399  	}
   400  
   401  	cmd := &packer.RemoteCmd{Command: cmdText}
   402  	if err := cmd.StartWithUi(comm, ui); err != nil {
   403  		return err
   404  	}
   405  	if cmd.ExitStatus != 0 {
   406  		return fmt.Errorf(
   407  			"Non-zero exit status. See output above for more info.\n\n"+
   408  				"Command: %s",
   409  			cmdText)
   410  	}
   411  
   412  	return nil
   413  }
   414  
   415  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   416  	ui.Message(fmt.Sprintf("Removing directory: %s", dir))
   417  
   418  	rmCmd := fmt.Sprintf("rm -rf '%s'", dir)
   419  	if !p.config.PreventSudo {
   420  		rmCmd = "sudo " + rmCmd
   421  	}
   422  
   423  	cmd := &packer.RemoteCmd{
   424  		Command: rmCmd,
   425  	}
   426  
   427  	if err := cmd.StartWithUi(comm, ui); err != nil {
   428  		return err
   429  	}
   430  
   431  	return nil
   432  }
   433  
   434  func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
   435  	p.config.ctx.Data = &ExecuteTemplate{
   436  		ConfigPath: config,
   437  		JsonPath:   json,
   438  		Sudo:       !p.config.PreventSudo,
   439  	}
   440  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   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  	p.config.ctx.Data = &InstallChefTemplate{
   466  		Sudo: !p.config.PreventSudo,
   467  	}
   468  	command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx)
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	cmd := &packer.RemoteCmd{Command: command}
   474  	if err := cmd.StartWithUi(comm, ui); err != nil {
   475  		return err
   476  	}
   477  
   478  	if cmd.ExitStatus != 0 {
   479  		return fmt.Errorf(
   480  			"Install script exited with non-zero exit status %d", cmd.ExitStatus)
   481  	}
   482  
   483  	return nil
   484  }
   485  
   486  func (p *Provisioner) copyValidationKey(ui packer.Ui, comm packer.Communicator, remotePath string) error {
   487  	ui.Message("Uploading validation key...")
   488  
   489  	// First upload the validation key to a writable location
   490  	f, err := os.Open(p.config.ValidationKeyPath)
   491  	if err != nil {
   492  		return err
   493  	}
   494  	defer f.Close()
   495  
   496  	if err := comm.Upload(remotePath, f, nil); err != nil {
   497  		return err
   498  	}
   499  
   500  	return nil
   501  }
   502  
   503  func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) {
   504  	if current == nil {
   505  		return nil, nil
   506  	}
   507  
   508  	switch c := current.(type) {
   509  	case []interface{}:
   510  		val := make([]interface{}, len(c))
   511  		for i, v := range c {
   512  			var err error
   513  			val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v)
   514  			if err != nil {
   515  				return nil, err
   516  			}
   517  		}
   518  
   519  		return val, nil
   520  	case []uint8:
   521  		return string(c), nil
   522  	case map[interface{}]interface{}:
   523  		val := make(map[string]interface{})
   524  		for k, v := range c {
   525  			ks, ok := k.(string)
   526  			if !ok {
   527  				return nil, fmt.Errorf("%s: key is not string", key)
   528  			}
   529  
   530  			var err error
   531  			val[ks], err = p.deepJsonFix(
   532  				fmt.Sprintf("%s.%s", key, ks), v)
   533  			if err != nil {
   534  				return nil, err
   535  			}
   536  		}
   537  
   538  		return val, nil
   539  	default:
   540  		return current, nil
   541  	}
   542  }
   543  
   544  func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
   545  	jsonBytes, err := json.Marshal(p.config.Json)
   546  	if err != nil {
   547  		// This really shouldn't happen since we literally just unmarshalled
   548  		panic(err)
   549  	}
   550  
   551  	// Copy the user variables so that we can restore them later, and
   552  	// make sure we make the quotes JSON-friendly in the user variables.
   553  	originalUserVars := make(map[string]string)
   554  	for k, v := range p.config.ctx.UserVariables {
   555  		originalUserVars[k] = v
   556  	}
   557  
   558  	// Make sure we reset them no matter what
   559  	defer func() {
   560  		p.config.ctx.UserVariables = originalUserVars
   561  	}()
   562  
   563  	// Make the current user variables JSON string safe.
   564  	for k, v := range p.config.ctx.UserVariables {
   565  		v = strings.Replace(v, `\`, `\\`, -1)
   566  		v = strings.Replace(v, `"`, `\"`, -1)
   567  		p.config.ctx.UserVariables[k] = v
   568  	}
   569  
   570  	// Process the bytes with the template processor
   571  	p.config.ctx.Data = nil
   572  	jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  
   577  	var result map[string]interface{}
   578  	if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil {
   579  		return nil, err
   580  	}
   581  
   582  	return result, nil
   583  }
   584  
   585  var DefaultConfigTemplate = `
   586  log_level        :info
   587  log_location     STDOUT
   588  chef_server_url  "{{.ServerUrl}}"
   589  client_key       "{{.ClientKey}}"
   590  {{if ne .ValidationClientName ""}}
   591  validation_client_name "{{.ValidationClientName}}"
   592  {{else}}
   593  validation_client_name "chef-validator"
   594  {{end}}
   595  {{if ne .ValidationKeyPath ""}}
   596  validation_key "{{.ValidationKeyPath}}"
   597  {{end}}
   598  node_name "{{.NodeName}}"
   599  {{if ne .ChefEnvironment ""}}
   600  environment "{{.ChefEnvironment}}"
   601  {{end}}
   602  {{if ne .SslVerifyMode ""}}
   603  ssl_verify_mode :{{.SslVerifyMode}}
   604  {{end}}
   605  `
   606  
   607  var DefaultKnifeTemplate = `
   608  log_level        :info
   609  log_location     STDOUT
   610  chef_server_url  "{{.ServerUrl}}"
   611  client_key       "{{.ClientKey}}"
   612  node_name "{{.NodeName}}"
   613  {{if ne .SslVerifyMode ""}}
   614  ssl_verify_mode :{{.SslVerifyMode}}
   615  {{end}}
   616  `