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