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