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