github.com/jerryclinesmith/packer@v0.3.7/provisioner/chef-solo/provisioner.go (about)

     1  // This package implements a provisioner for Packer that uses
     2  // Chef to provision the remote machine, specifically with chef-solo (that is,
     3  // without a Chef server).
     4  package chefsolo
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"github.com/mitchellh/packer/common"
    11  	"github.com/mitchellh/packer/packer"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  type Config struct {
    18  	common.PackerConfig `mapstructure:",squash"`
    19  
    20  	CookbookPaths       []string `mapstructure:"cookbook_paths"`
    21  	ExecuteCommand      string   `mapstructure:"execute_command"`
    22  	InstallCommand      string   `mapstructure:"install_command"`
    23  	RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"`
    24  	Json                map[string]interface{}
    25  	PreventSudo         bool     `mapstructure:"prevent_sudo"`
    26  	RunList             []string `mapstructure:"run_list"`
    27  	SkipInstall         bool     `mapstructure:"skip_install"`
    28  	StagingDir          string   `mapstructure:"staging_directory"`
    29  
    30  	tpl *packer.ConfigTemplate
    31  }
    32  
    33  type Provisioner struct {
    34  	config Config
    35  }
    36  
    37  type ConfigTemplate struct {
    38  	CookbookPaths string
    39  }
    40  
    41  type ExecuteTemplate struct {
    42  	ConfigPath string
    43  	JsonPath   string
    44  	Sudo       bool
    45  }
    46  
    47  type InstallChefTemplate struct {
    48  	Sudo bool
    49  }
    50  
    51  func (p *Provisioner) Prepare(raws ...interface{}) error {
    52  	md, err := common.DecodeConfig(&p.config, raws...)
    53  	if err != nil {
    54  		return err
    55  	}
    56  
    57  	p.config.tpl, err = packer.NewConfigTemplate()
    58  	if err != nil {
    59  		return err
    60  	}
    61  	p.config.tpl.UserVars = p.config.PackerUserVars
    62  
    63  	if p.config.ExecuteCommand == "" {
    64  		p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}"
    65  	}
    66  
    67  	if p.config.InstallCommand == "" {
    68  		p.config.InstallCommand = "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo {{end}}bash"
    69  	}
    70  
    71  	if p.config.RunList == nil {
    72  		p.config.RunList = make([]string, 0)
    73  	}
    74  
    75  	if p.config.StagingDir == "" {
    76  		p.config.StagingDir = "/tmp/packer-chef-solo"
    77  	}
    78  
    79  	// Accumulate any errors
    80  	errs := common.CheckUnusedConfig(md)
    81  
    82  	templates := map[string]*string{
    83  		"staging_dir": &p.config.StagingDir,
    84  	}
    85  
    86  	for n, ptr := range templates {
    87  		var err error
    88  		*ptr, err = p.config.tpl.Process(*ptr, nil)
    89  		if err != nil {
    90  			errs = packer.MultiErrorAppend(
    91  				errs, fmt.Errorf("Error processing %s: %s", n, err))
    92  		}
    93  	}
    94  
    95  	sliceTemplates := map[string][]string{
    96  		"cookbook_paths":        p.config.CookbookPaths,
    97  		"remote_cookbook_paths": p.config.RemoteCookbookPaths,
    98  		"run_list":              p.config.RunList,
    99  	}
   100  
   101  	for n, slice := range sliceTemplates {
   102  		for i, elem := range slice {
   103  			var err error
   104  			slice[i], err = p.config.tpl.Process(elem, nil)
   105  			if err != nil {
   106  				errs = packer.MultiErrorAppend(
   107  					errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
   108  			}
   109  		}
   110  	}
   111  
   112  	validates := map[string]*string{
   113  		"execute_command": &p.config.ExecuteCommand,
   114  		"install_command": &p.config.InstallCommand,
   115  	}
   116  
   117  	for n, ptr := range validates {
   118  		if err := p.config.tpl.Validate(*ptr); err != nil {
   119  			errs = packer.MultiErrorAppend(
   120  				errs, fmt.Errorf("Error parsing %s: %s", n, err))
   121  		}
   122  	}
   123  
   124  	for _, path := range p.config.CookbookPaths {
   125  		pFileInfo, err := os.Stat(path)
   126  
   127  		if err != nil || !pFileInfo.IsDir() {
   128  			errs = packer.MultiErrorAppend(
   129  				errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err))
   130  		}
   131  	}
   132  
   133  	// Process the user variables within the JSON and set the JSON.
   134  	// Do this early so that we can validate and show errors.
   135  	p.config.Json, err = p.processJsonUserVars()
   136  	if err != nil {
   137  		errs = packer.MultiErrorAppend(
   138  			errs, fmt.Errorf("Error processing user variables in JSON: %s", err))
   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  	if !p.config.SkipInstall {
   150  		if err := p.installChef(ui, comm); err != nil {
   151  			return fmt.Errorf("Error installing Chef: %s", err)
   152  		}
   153  	}
   154  
   155  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   156  		return fmt.Errorf("Error creating staging directory: %s", err)
   157  	}
   158  
   159  	cookbookPaths := make([]string, 0, len(p.config.CookbookPaths))
   160  	for i, path := range p.config.CookbookPaths {
   161  		targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i)
   162  		if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
   163  			return fmt.Errorf("Error uploading cookbooks: %s", err)
   164  		}
   165  
   166  		cookbookPaths = append(cookbookPaths, targetPath)
   167  	}
   168  
   169  	configPath, err := p.createConfig(ui, comm, cookbookPaths)
   170  	if err != nil {
   171  		return fmt.Errorf("Error creating Chef config file: %s", err)
   172  	}
   173  
   174  	jsonPath, err := p.createJson(ui, comm)
   175  	if err != nil {
   176  		return fmt.Errorf("Error creating JSON attributes: %s", err)
   177  	}
   178  
   179  	if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil {
   180  		return fmt.Errorf("Error executing Chef: %s", err)
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  func (p *Provisioner) Cancel() {
   187  	// Just hard quit. It isn't a big deal if what we're doing keeps
   188  	// running on the other side.
   189  	os.Exit(0)
   190  }
   191  
   192  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   193  	if err := p.createDir(ui, comm, dst); err != nil {
   194  		return err
   195  	}
   196  
   197  	// Make sure there is a trailing "/" so that the directory isn't
   198  	// created on the other side.
   199  	if src[len(src)-1] != '/' {
   200  		src = src + "/"
   201  	}
   202  
   203  	return comm.UploadDir(dst, src, nil)
   204  }
   205  
   206  func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string) (string, error) {
   207  	ui.Message("Creating configuration file 'solo.rb'")
   208  
   209  	cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks))
   210  	for i, path := range p.config.RemoteCookbookPaths {
   211  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   212  	}
   213  
   214  	for i, path := range localCookbooks {
   215  		i = len(p.config.RemoteCookbookPaths) + i
   216  		cookbook_paths[i] = fmt.Sprintf(`"%s"`, path)
   217  	}
   218  
   219  	configString, err := p.config.tpl.Process(DefaultConfigTemplate, &ConfigTemplate{
   220  		CookbookPaths: strings.Join(cookbook_paths, ","),
   221  	})
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  
   226  	remotePath := filepath.Join(p.config.StagingDir, "solo.rb")
   227  	if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString))); err != nil {
   228  		return "", err
   229  	}
   230  
   231  	return remotePath, nil
   232  }
   233  
   234  func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) {
   235  	ui.Message("Creating JSON attribute file")
   236  
   237  	jsonData := make(map[string]interface{})
   238  
   239  	// Copy the configured JSON
   240  	for k, v := range p.config.Json {
   241  		jsonData[k] = v
   242  	}
   243  
   244  	// Set the run list if it was specified
   245  	if len(p.config.RunList) > 0 {
   246  		jsonData["run_list"] = p.config.RunList
   247  	}
   248  
   249  	jsonBytes, err := json.MarshalIndent(jsonData, "", "  ")
   250  	if err != nil {
   251  		return "", err
   252  	}
   253  
   254  	// Upload the bytes
   255  	remotePath := filepath.Join(p.config.StagingDir, "node.json")
   256  	if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes)); err != nil {
   257  		return "", err
   258  	}
   259  
   260  	return remotePath, nil
   261  }
   262  
   263  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   264  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   265  	cmd := &packer.RemoteCmd{
   266  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   267  	}
   268  
   269  	if err := cmd.StartWithUi(comm, ui); err != nil {
   270  		return err
   271  	}
   272  
   273  	if cmd.ExitStatus != 0 {
   274  		return fmt.Errorf("Non-zero exit status.")
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error {
   281  	command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteTemplate{
   282  		ConfigPath: config,
   283  		JsonPath:   json,
   284  		Sudo:       !p.config.PreventSudo,
   285  	})
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	ui.Message(fmt.Sprintf("Executing Chef: %s", command))
   291  
   292  	cmd := &packer.RemoteCmd{
   293  		Command: command,
   294  	}
   295  
   296  	if err := cmd.StartWithUi(comm, ui); err != nil {
   297  		return err
   298  	}
   299  
   300  	if cmd.ExitStatus != 0 {
   301  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error {
   308  	ui.Message("Installing Chef...")
   309  
   310  	command, err := p.config.tpl.Process(p.config.InstallCommand, &InstallChefTemplate{
   311  		Sudo: !p.config.PreventSudo,
   312  	})
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	cmd := &packer.RemoteCmd{Command: command}
   318  	if err := cmd.StartWithUi(comm, ui); err != nil {
   319  		return err
   320  	}
   321  
   322  	if cmd.ExitStatus != 0 {
   323  		return fmt.Errorf(
   324  			"Install script exited with non-zero exit status %d", cmd.ExitStatus)
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) {
   331  	jsonBytes, err := json.Marshal(p.config.Json)
   332  	if err != nil {
   333  		// This really shouldn't happen since we literally just unmarshalled
   334  		panic(err)
   335  	}
   336  
   337  	// Copy the user variables so that we can restore them later, and
   338  	// make sure we make the quotes JSON-friendly in the user variables.
   339  	originalUserVars := make(map[string]string)
   340  	for k, v := range p.config.tpl.UserVars {
   341  		originalUserVars[k] = v
   342  	}
   343  
   344  	// Make sure we reset them no matter what
   345  	defer func() {
   346  		p.config.tpl.UserVars = originalUserVars
   347  	}()
   348  
   349  	// Make the current user variables JSON string safe.
   350  	for k, v := range p.config.tpl.UserVars {
   351  		v = strings.Replace(v, `\`, `\\`, -1)
   352  		v = strings.Replace(v, `"`, `\"`, -1)
   353  		p.config.tpl.UserVars[k] = v
   354  	}
   355  
   356  	// Process the bytes with the template processor
   357  	jsonBytesProcessed, err := p.config.tpl.Process(string(jsonBytes), nil)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	var result map[string]interface{}
   363  	if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	return result, nil
   368  }
   369  
   370  var DefaultConfigTemplate = `
   371  cookbook_path [{{.CookbookPaths}}]
   372  `