github.com/jbronn/packer@v0.1.6-0.20140120165540-8a1364dbd817/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 _, kv := range p.config.Vars {
   174  		vs := strings.Split(kv, "=")
   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  		}
   179  	}
   180  
   181  	if p.config.RawStartRetryTimeout != "" {
   182  		p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
   183  		if err != nil {
   184  			errs = packer.MultiErrorAppend(
   185  				errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
   186  		}
   187  	}
   188  
   189  	if errs != nil && len(errs.Errors) > 0 {
   190  		return errs
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   197  	scripts := make([]string, len(p.config.Scripts))
   198  	copy(scripts, p.config.Scripts)
   199  
   200  	// If we have an inline script, then turn that into a temporary
   201  	// shell script and use that.
   202  	if p.config.Inline != nil {
   203  		tf, err := ioutil.TempFile("", "packer-shell")
   204  		if err != nil {
   205  			return fmt.Errorf("Error preparing shell script: %s", err)
   206  		}
   207  		defer os.Remove(tf.Name())
   208  
   209  		// Set the path to the temporary file
   210  		scripts = append(scripts, tf.Name())
   211  
   212  		// Write our contents to it
   213  		writer := bufio.NewWriter(tf)
   214  		writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
   215  		for _, command := range p.config.Inline {
   216  			if _, err := writer.WriteString(command + "\n"); err != nil {
   217  				return fmt.Errorf("Error preparing shell script: %s", err)
   218  			}
   219  		}
   220  
   221  		if err := writer.Flush(); err != nil {
   222  			return fmt.Errorf("Error preparing shell script: %s", err)
   223  		}
   224  
   225  		tf.Close()
   226  	}
   227  
   228  	// Build our variables up by adding in the build name and builder type
   229  	envVars := make([]string, len(p.config.Vars)+2)
   230  	envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName
   231  	envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType
   232  	copy(envVars[2:], p.config.Vars)
   233  
   234  	for _, path := range scripts {
   235  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   236  
   237  		log.Printf("Opening %s for reading", path)
   238  		f, err := os.Open(path)
   239  		if err != nil {
   240  			return fmt.Errorf("Error opening shell script: %s", err)
   241  		}
   242  		defer f.Close()
   243  
   244  		// Flatten the environment variables
   245  		flattendVars := strings.Join(envVars, " ")
   246  
   247  		// Compile the command
   248  		command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{
   249  			Vars: flattendVars,
   250  			Path: p.config.RemotePath,
   251  		})
   252  		if err != nil {
   253  			return fmt.Errorf("Error processing command: %s", err)
   254  		}
   255  
   256  		// Upload the file and run the command. Do this in the context of
   257  		// a single retryable function so that we don't end up with
   258  		// the case that the upload succeeded, a restart is initiated,
   259  		// and then the command is executed but the file doesn't exist
   260  		// any longer.
   261  		var cmd *packer.RemoteCmd
   262  		err = p.retryable(func() error {
   263  			if _, err := f.Seek(0, 0); err != nil {
   264  				return err
   265  			}
   266  
   267  			var r io.Reader = f
   268  			if !p.config.Binary {
   269  				r = &UnixReader{Reader: r}
   270  			}
   271  
   272  			if err := comm.Upload(p.config.RemotePath, r); err != nil {
   273  				return fmt.Errorf("Error uploading script: %s", err)
   274  			}
   275  
   276  			cmd = &packer.RemoteCmd{Command: command}
   277  			return cmd.StartWithUi(comm, ui)
   278  		})
   279  		if err != nil {
   280  			return err
   281  		}
   282  
   283  		// Close the original file since we copied it
   284  		f.Close()
   285  
   286  		if cmd.ExitStatus != 0 {
   287  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   288  		}
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  func (p *Provisioner) Cancel() {
   295  	// Just hard quit. It isn't a big deal if what we're doing keeps
   296  	// running on the other side.
   297  	os.Exit(0)
   298  }
   299  
   300  // retryable will retry the given function over and over until a
   301  // non-error is returned.
   302  func (p *Provisioner) retryable(f func() error) error {
   303  	startTimeout := time.After(p.config.startRetryTimeout)
   304  	for {
   305  		var err error
   306  		if err = f(); err == nil {
   307  			return nil
   308  		}
   309  
   310  		// Create an error and log it
   311  		err = fmt.Errorf("Retryable error: %s", err)
   312  		log.Printf(err.Error())
   313  
   314  		// Check if we timed out, otherwise we retry. It is safe to
   315  		// retry since the only error case above is if the command
   316  		// failed to START.
   317  		select {
   318  		case <-startTimeout:
   319  			return err
   320  		default:
   321  			time.Sleep(2 * time.Second)
   322  		}
   323  	}
   324  }