github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/provisioner/shell/provisioner.go (about)

     1  // This package implements a provisioner for Packer that executes
     2  // shell scripts within the remote machine.
     3  package shell
     4  
     5  import (
     6  	"bufio"
     7  	"errors"
     8  	"fmt"
     9  	"github.com/mitchellh/packer/common"
    10  	"github.com/mitchellh/packer/packer"
    11  	"io"
    12  	"io/ioutil"
    13  	"log"
    14  	"os"
    15  	"strings"
    16  	"time"
    17  )
    18  
    19  const DefaultRemotePath = "/tmp/script.sh"
    20  
    21  type config struct {
    22  	common.PackerConfig `mapstructure:",squash"`
    23  
    24  	// If true, the script contains binary and line endings will not be
    25  	// converted from Windows to Unix-style.
    26  	Binary bool
    27  
    28  	// An inline script to execute. Multiple strings are all executed
    29  	// in the context of a single shell.
    30  	Inline []string
    31  
    32  	// The shebang value used when running inline scripts.
    33  	InlineShebang string `mapstructure:"inline_shebang"`
    34  
    35  	// The local path of the shell script to upload and execute.
    36  	Script string
    37  
    38  	// An array of multiple scripts to run.
    39  	Scripts []string
    40  
    41  	// An array of environment variables that will be injected before
    42  	// your command(s) are executed.
    43  	Vars []string `mapstructure:"environment_vars"`
    44  
    45  	// The remote path where the local shell script will be uploaded to.
    46  	// This should be set to a writable file that is in a pre-existing directory.
    47  	RemotePath string `mapstructure:"remote_path"`
    48  
    49  	// The command used to execute the script. The '{{ .Path }}' variable
    50  	// should be used to specify where the script goes, {{ .Vars }}
    51  	// can be used to inject the environment_vars into the environment.
    52  	ExecuteCommand string `mapstructure:"execute_command"`
    53  
    54  	// The timeout for retrying to start the process. Until this timeout
    55  	// is reached, if the provisioner can't start a process, it retries.
    56  	// This can be set high to allow for reboots.
    57  	RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
    58  
    59  	startRetryTimeout time.Duration
    60  	tpl               *packer.ConfigTemplate
    61  }
    62  
    63  type Provisioner struct {
    64  	config config
    65  }
    66  
    67  type ExecuteCommandTemplate struct {
    68  	Vars string
    69  	Path string
    70  }
    71  
    72  func (p *Provisioner) Prepare(raws ...interface{}) error {
    73  	md, err := common.DecodeConfig(&p.config, raws...)
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	p.config.tpl, err = packer.NewConfigTemplate()
    79  	if err != nil {
    80  		return err
    81  	}
    82  	p.config.tpl.UserVars = p.config.PackerUserVars
    83  
    84  	// Accumulate any errors
    85  	errs := common.CheckUnusedConfig(md)
    86  
    87  	if p.config.ExecuteCommand == "" {
    88  		p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
    89  	}
    90  
    91  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
    92  		p.config.Inline = nil
    93  	}
    94  
    95  	if p.config.InlineShebang == "" {
    96  		p.config.InlineShebang = "/bin/sh"
    97  	}
    98  
    99  	if p.config.RawStartRetryTimeout == "" {
   100  		p.config.RawStartRetryTimeout = "5m"
   101  	}
   102  
   103  	if p.config.RemotePath == "" {
   104  		p.config.RemotePath = DefaultRemotePath
   105  	}
   106  
   107  	if p.config.Scripts == nil {
   108  		p.config.Scripts = make([]string, 0)
   109  	}
   110  
   111  	if p.config.Vars == nil {
   112  		p.config.Vars = make([]string, 0)
   113  	}
   114  
   115  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   116  		errs = packer.MultiErrorAppend(errs,
   117  			errors.New("Only one of script or scripts can be specified."))
   118  	}
   119  
   120  	if p.config.Script != "" {
   121  		p.config.Scripts = []string{p.config.Script}
   122  	}
   123  
   124  	templates := map[string]*string{
   125  		"inline_shebang":      &p.config.InlineShebang,
   126  		"script":              &p.config.Script,
   127  		"start_retry_timeout": &p.config.RawStartRetryTimeout,
   128  		"remote_path":         &p.config.RemotePath,
   129  	}
   130  
   131  	for n, ptr := range templates {
   132  		var err error
   133  		*ptr, err = p.config.tpl.Process(*ptr, nil)
   134  		if err != nil {
   135  			errs = packer.MultiErrorAppend(
   136  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   137  		}
   138  	}
   139  
   140  	sliceTemplates := map[string][]string{
   141  		"inline":           p.config.Inline,
   142  		"scripts":          p.config.Scripts,
   143  		"environment_vars": p.config.Vars,
   144  	}
   145  
   146  	for n, slice := range sliceTemplates {
   147  		for i, elem := range slice {
   148  			var err error
   149  			slice[i], err = p.config.tpl.Process(elem, nil)
   150  			if err != nil {
   151  				errs = packer.MultiErrorAppend(
   152  					errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
   153  			}
   154  		}
   155  	}
   156  
   157  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   158  		errs = packer.MultiErrorAppend(errs,
   159  			errors.New("Either a script file or inline script must be specified."))
   160  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   161  		errs = packer.MultiErrorAppend(errs,
   162  			errors.New("Only a script file or an inline script can be specified, not both."))
   163  	}
   164  
   165  	for _, path := range p.config.Scripts {
   166  		if _, err := os.Stat(path); err != nil {
   167  			errs = packer.MultiErrorAppend(errs,
   168  				fmt.Errorf("Bad script '%s': %s", path, err))
   169  		}
   170  	}
   171  
   172  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   173  	for idx, kv := range p.config.Vars {
   174  		vs := strings.SplitN(kv, "=", 2)
   175  		if len(vs) != 2 || vs[0] == "" {
   176  			errs = packer.MultiErrorAppend(errs,
   177  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   178  		} else {
   179  			// Single quote env var values
   180  			p.config.Vars[idx] = fmt.Sprintf("%s='%s'", vs[0], vs[1])
   181  		}
   182  	}
   183  
   184  	if p.config.RawStartRetryTimeout != "" {
   185  		p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
   186  		if err != nil {
   187  			errs = packer.MultiErrorAppend(
   188  				errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
   189  		}
   190  	}
   191  
   192  	if errs != nil && len(errs.Errors) > 0 {
   193  		return errs
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   200  	scripts := make([]string, len(p.config.Scripts))
   201  	copy(scripts, p.config.Scripts)
   202  
   203  	// If we have an inline script, then turn that into a temporary
   204  	// shell script and use that.
   205  	if p.config.Inline != nil {
   206  		tf, err := ioutil.TempFile("", "packer-shell")
   207  		if err != nil {
   208  			return fmt.Errorf("Error preparing shell script: %s", err)
   209  		}
   210  		defer os.Remove(tf.Name())
   211  
   212  		// Set the path to the temporary file
   213  		scripts = append(scripts, tf.Name())
   214  
   215  		// Write our contents to it
   216  		writer := bufio.NewWriter(tf)
   217  		writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
   218  		for _, command := range p.config.Inline {
   219  			if _, err := writer.WriteString(command + "\n"); err != nil {
   220  				return fmt.Errorf("Error preparing shell script: %s", err)
   221  			}
   222  		}
   223  
   224  		if err := writer.Flush(); err != nil {
   225  			return fmt.Errorf("Error preparing shell script: %s", err)
   226  		}
   227  
   228  		tf.Close()
   229  	}
   230  
   231  	// Build our variables up by adding in the build name and builder type
   232  	envVars := make([]string, len(p.config.Vars)+2)
   233  	envVars[0] = fmt.Sprintf("PACKER_BUILD_NAME='%s'", p.config.PackerBuildName)
   234  	envVars[1] = fmt.Sprintf("PACKER_BUILDER_TYPE='%s'", p.config.PackerBuilderType)
   235  	copy(envVars[2:], p.config.Vars)
   236  
   237  	for _, path := range scripts {
   238  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   239  
   240  		log.Printf("Opening %s for reading", path)
   241  		f, err := os.Open(path)
   242  		if err != nil {
   243  			return fmt.Errorf("Error opening shell script: %s", err)
   244  		}
   245  		defer f.Close()
   246  
   247  		// Flatten the environment variables
   248  		flattendVars := strings.Join(envVars, " ")
   249  
   250  		// Compile the command
   251  		command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{
   252  			Vars: flattendVars,
   253  			Path: p.config.RemotePath,
   254  		})
   255  		if err != nil {
   256  			return fmt.Errorf("Error processing command: %s", err)
   257  		}
   258  
   259  		// Upload the file and run the command. Do this in the context of
   260  		// a single retryable function so that we don't end up with
   261  		// the case that the upload succeeded, a restart is initiated,
   262  		// and then the command is executed but the file doesn't exist
   263  		// any longer.
   264  		var cmd *packer.RemoteCmd
   265  		err = p.retryable(func() error {
   266  			if _, err := f.Seek(0, 0); err != nil {
   267  				return err
   268  			}
   269  
   270  			var r io.Reader = f
   271  			if !p.config.Binary {
   272  				r = &UnixReader{Reader: r}
   273  			}
   274  
   275  			if err := comm.Upload(p.config.RemotePath, r, nil); err != nil {
   276  				return fmt.Errorf("Error uploading script: %s", err)
   277  			}
   278  
   279  			cmd = &packer.RemoteCmd{
   280  				Command: fmt.Sprintf("chmod 0777 %s", p.config.RemotePath),
   281  			}
   282  			if err := comm.Start(cmd); err != nil {
   283  				return fmt.Errorf(
   284  					"Error chmodding script file to 0777 in remote "+
   285  						"machine: %s", err)
   286  			}
   287  			cmd.Wait()
   288  
   289  			cmd = &packer.RemoteCmd{Command: command}
   290  			return cmd.StartWithUi(comm, ui)
   291  		})
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		// Close the original file since we copied it
   297  		f.Close()
   298  
   299  		if cmd.ExitStatus != 0 {
   300  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   301  		}
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func (p *Provisioner) Cancel() {
   308  	// Just hard quit. It isn't a big deal if what we're doing keeps
   309  	// running on the other side.
   310  	os.Exit(0)
   311  }
   312  
   313  // retryable will retry the given function over and over until a
   314  // non-error is returned.
   315  func (p *Provisioner) retryable(f func() error) error {
   316  	startTimeout := time.After(p.config.startRetryTimeout)
   317  	for {
   318  		var err error
   319  		if err = f(); err == nil {
   320  			return nil
   321  		}
   322  
   323  		// Create an error and log it
   324  		err = fmt.Errorf("Retryable error: %s", err)
   325  		log.Printf(err.Error())
   326  
   327  		// Check if we timed out, otherwise we retry. It is safe to
   328  		// retry since the only error case above is if the command
   329  		// failed to START.
   330  		select {
   331  		case <-startTimeout:
   332  			return err
   333  		default:
   334  			time.Sleep(2 * time.Second)
   335  		}
   336  	}
   337  }