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