github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/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  	if !p.config.SkipCleanNode {
   191  		if err2 := p.cleanNode(ui, comm, nodeName); err2 != nil {
   192  			return fmt.Errorf("Error cleaning up chef node: %s", err2)
   193  		}
   194  	}
   195  
   196  	if !p.config.SkipCleanClient {
   197  		if err2 := p.cleanClient(ui, comm, nodeName); err2 != nil {
   198  			return fmt.Errorf("Error cleaning up chef client: %s", err2)
   199  		}
   200  	}
   201  
   202  	if err != nil {
   203  		return fmt.Errorf("Error executing Chef: %s", err)
   204  	}
   205  
   206  	if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
   207  		return fmt.Errorf("Error removing /etc/chef directory: %s", err)
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  func (p *Provisioner) Cancel() {
   214  	// Just hard quit. It isn't a big deal if what we're doing keeps
   215  	// running on the other side.
   216  	os.Exit(0)
   217  }
   218  
   219  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   220  	if err := p.createDir(ui, comm, dst); err != nil {
   221  		return err
   222  	}
   223  
   224  	// Make sure there is a trailing "/" so that the directory isn't
   225  	// created on the other side.
   226  	if src[len(src)-1] != '/' {
   227  		src = src + "/"
   228  	}
   229  
   230  	return comm.UploadDir(dst, src, nil)
   231  }
   232  
   233  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) {
   234  	ui.Message("Creating configuration file 'client.rb'")
   235  
   236  	// Read the template
   237  	tpl := DefaultConfigTemplate
   238  	if p.config.ConfigTemplate != "" {
   239  		f, err := os.Open(p.config.ConfigTemplate)
   240  		if err != nil {
   241  			return "", err
   242  		}
   243  		defer f.Close()
   244  
   245  		tplBytes, err := ioutil.ReadAll(f)
   246  		if err != nil {
   247  			return "", err
   248  		}
   249  
   250  		tpl = string(tplBytes)
   251  	}
   252  
   253  	ctx := p.config.ctx
   254  	ctx.Data = &ConfigTemplate{
   255  		NodeName:             nodeName,
   256  		ServerUrl:            serverUrl,
   257  		ClientKey:            clientKey,
   258  		ValidationKeyPath:    remoteKeyPath,
   259  		ValidationClientName: validationClientName,
   260  		ChefEnvironment:      chefEnvironment,
   261  		SslVerifyMode:        sslVerifyMode,
   262  	}
   263  	configString, err := interpolate.Render(tpl, &ctx)
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  
   268  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb"))
   269  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil {
   270  		return "", err
   271  	}
   272  
   273  	return remotePath, nil
   274  }
   275  
   276  func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
   277  	ui.Message("Creating JSON attribute file")
   278  
   279  	jsonData := make(map[string]interface{})
   280  
   281  	// Copy the configured JSON
   282  	for k, v := range p.config.Json {
   283  		jsonData[k] = v
   284  	}
   285  
   286  	// Set the run list if it was specified
   287  	if len(p.config.RunList) > 0 {
   288  		jsonData["run_list"] = p.config.RunList
   289  	}
   290  
   291  	jsonBytes, err := json.MarshalIndent(jsonData, "", "  ")
   292  	if err != nil {
   293  		return "", err
   294  	}
   295  
   296  	// Upload the bytes
   297  	remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json"))
   298  	if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil {
   299  		return "", err
   300  	}
   301  
   302  	return remotePath, nil
   303  }
   304  
   305  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   306  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   307  
   308  	mkdirCmd := fmt.Sprintf("mkdir -p '%s'", dir)
   309  	if !p.config.PreventSudo {
   310  		mkdirCmd = "sudo " + mkdirCmd
   311  	}
   312  
   313  	cmd := &packer.RemoteCmd{Command: mkdirCmd}
   314  	if err := cmd.StartWithUi(comm, ui); err != nil {
   315  		return err
   316  	}
   317  	if cmd.ExitStatus != 0 {
   318  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   319  	}
   320  
   321  	// Chmod the directory to 0777 just so that we can access it as our user
   322  	mkdirCmd = fmt.Sprintf("chmod 0777 '%s'", dir)
   323  	if !p.config.PreventSudo {
   324  		mkdirCmd = "sudo " + mkdirCmd
   325  	}
   326  	cmd = &packer.RemoteCmd{Command: mkdirCmd}
   327  	if err := cmd.StartWithUi(comm, ui); err != nil {
   328  		return err
   329  	}
   330  	if cmd.ExitStatus != 0 {
   331  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   332  	}
   333  
   334  	return nil
   335  }
   336  
   337  func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string) error {
   338  	ui.Say("Cleaning up chef node...")
   339  	args := []string{"node", "delete", node}
   340  	if err := p.knifeExec(ui, comm, node, args); err != nil {
   341  		return fmt.Errorf("Failed to cleanup node: %s", err)
   342  	}
   343  
   344  	return nil
   345  }
   346  
   347  func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string) error {
   348  	ui.Say("Cleaning up chef client...")
   349  	args := []string{"client", "delete", node}
   350  	if err := p.knifeExec(ui, comm, node, args); err != nil {
   351  		return fmt.Errorf("Failed to cleanup client: %s", err)
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, args []string) error {
   358  	flags := []string{
   359  		"-y",
   360  		"-s", fmt.Sprintf("'%s'", p.config.ServerUrl),
   361  		"-k", fmt.Sprintf("'%s'", p.config.ClientKey),
   362  		"-u", fmt.Sprintf("'%s'", node),
   363  	}
   364  
   365  	cmdText := fmt.Sprintf(
   366  		"knife %s %s", strings.Join(args, " "), strings.Join(flags, " "))
   367  	if !p.config.PreventSudo {
   368  		cmdText = "sudo " + cmdText
   369  	}
   370  
   371  	cmd := &packer.RemoteCmd{Command: cmdText}
   372  	if err := cmd.StartWithUi(comm, ui); err != nil {
   373  		return err
   374  	}
   375  	if cmd.ExitStatus != 0 {
   376  		return fmt.Errorf(
   377  			"Non-zero exit status. See output above for more info.\n\n"+
   378  				"Command: %s",
   379  			cmdText)
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   386  	ui.Message(fmt.Sprintf("Removing directory: %s", dir))
   387  
   388  	rmCmd := fmt.Sprintf("rm -rf '%s'", dir)
   389  	if !p.config.PreventSudo {
   390  		rmCmd = "sudo " + rmCmd
   391  	}
   392  
   393  	cmd := &packer.RemoteCmd{
   394  		Command: rmCmd,
   395  	}
   396  
   397  	if err := cmd.StartWithUi(comm, ui); err != nil {
   398  		return err
   399  	}
   400  
   401  	return nil
   402  }
   403  
   404  func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
   405  	p.config.ctx.Data = &ExecuteTemplate{
   406  		ConfigPath: config,
   407  		JsonPath:   json,
   408  		Sudo:       !p.config.PreventSudo,
   409  	}
   410  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   411  	if err != nil {
   412  		return err
   413  	}
   414  
   415  	ui.Message(fmt.Sprintf("Executing Chef: %s", command))
   416  
   417  	cmd := &packer.RemoteCmd{
   418  		Command: command,
   419  	}
   420  
   421  	if err := cmd.StartWithUi(comm, ui); err != nil {
   422  		return err
   423  	}
   424  
   425  	if cmd.ExitStatus != 0 {
   426  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   427  	}
   428  
   429  	return nil
   430  }
   431  
   432  func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error {
   433  	ui.Message("Installing Chef...")
   434  
   435  	p.config.ctx.Data = &InstallChefTemplate{
   436  		Sudo: !p.config.PreventSudo,
   437  	}
   438  	command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx)
   439  	if err != nil {
   440  		return err
   441  	}
   442  
   443  	cmd := &packer.RemoteCmd{Command: command}
   444  	if err := cmd.StartWithUi(comm, ui); err != nil {
   445  		return err
   446  	}
   447  
   448  	if cmd.ExitStatus != 0 {
   449  		return fmt.Errorf(
   450  			"Install script exited with non-zero exit status %d", cmd.ExitStatus)
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  func (p *Provisioner) copyValidationKey(ui packer.Ui, comm packer.Communicator, remotePath string) error {
   457  	ui.Message("Uploading validation key...")
   458  
   459  	// First upload the validation key to a writable location
   460  	f, err := os.Open(p.config.ValidationKeyPath)
   461  	if err != nil {
   462  		return err
   463  	}
   464  	defer f.Close()
   465  
   466  	if err := comm.Upload(remotePath, f, nil); err != nil {
   467  		return err
   468  	}
   469  
   470  	return nil
   471  }
   472  
   473  func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) {
   474  	if current == nil {
   475  		return nil, nil
   476  	}
   477  
   478  	switch c := current.(type) {
   479  	case []interface{}:
   480  		val := make([]interface{}, len(c))
   481  		for i, v := range c {
   482  			var err error
   483  			val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v)
   484  			if err != nil {
   485  				return nil, err
   486  			}
   487  		}
   488  
   489  		return val, nil
   490  	case []uint8:
   491  		return string(c), nil
   492  	case map[interface{}]interface{}:
   493  		val := make(map[string]interface{})
   494  		for k, v := range c {
   495  			ks, ok := k.(string)
   496  			if !ok {
   497  				return nil, fmt.Errorf("%s: key is not string", key)
   498  			}
   499  
   500  			var err error
   501  			val[ks], err = p.deepJsonFix(
   502  				fmt.Sprintf("%s.%s", key, ks), v)
   503  			if err != nil {
   504  				return nil, err
   505  			}
   506  		}
   507  
   508  		return val, nil
   509  	default:
   510  		return current, nil
   511  	}
   512  }
   513  
   514  func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
   515  	jsonBytes, err := json.Marshal(p.config.Json)
   516  	if err != nil {
   517  		// This really shouldn't happen since we literally just unmarshalled
   518  		panic(err)
   519  	}
   520  
   521  	// Copy the user variables so that we can restore them later, and
   522  	// make sure we make the quotes JSON-friendly in the user variables.
   523  	originalUserVars := make(map[string]string)
   524  	for k, v := range p.config.ctx.UserVariables {
   525  		originalUserVars[k] = v
   526  	}
   527  
   528  	// Make sure we reset them no matter what
   529  	defer func() {
   530  		p.config.ctx.UserVariables = originalUserVars
   531  	}()
   532  
   533  	// Make the current user variables JSON string safe.
   534  	for k, v := range p.config.ctx.UserVariables {
   535  		v = strings.Replace(v, `\`, `\\`, -1)
   536  		v = strings.Replace(v, `"`, `\"`, -1)
   537  		p.config.ctx.UserVariables[k] = v
   538  	}
   539  
   540  	// Process the bytes with the template processor
   541  	p.config.ctx.Data = nil
   542  	jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx)
   543  	if err != nil {
   544  		return nil, err
   545  	}
   546  
   547  	var result map[string]interface{}
   548  	if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil {
   549  		return nil, err
   550  	}
   551  
   552  	return result, nil
   553  }
   554  
   555  var DefaultConfigTemplate = `
   556  log_level        :info
   557  log_location     STDOUT
   558  chef_server_url  "{{.ServerUrl}}"
   559  client_key       "{{.ClientKey}}"
   560  {{if ne .ValidationClientName ""}}
   561  validation_client_name "{{.ValidationClientName}}"
   562  {{else}}
   563  validation_client_name "chef-validator"
   564  {{end}}
   565  {{if ne .ValidationKeyPath ""}}
   566  validation_key "{{.ValidationKeyPath}}"
   567  {{end}}
   568  node_name "{{.NodeName}}"
   569  {{if ne .ChefEnvironment ""}}
   570  environment "{{.ChefEnvironment}}"
   571  {{end}}
   572  {{if ne .SslVerifyMode ""}}
   573  ssl_verify_mode :{{.SslVerifyMode}}
   574  {{end}}
   575  `