github.com/aclaygray/packer@v1.3.2/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  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/packer/common"
    19  	"github.com/hashicorp/packer/helper/config"
    20  	"github.com/hashicorp/packer/packer"
    21  	"github.com/hashicorp/packer/template/interpolate"
    22  )
    23  
    24  type Config struct {
    25  	common.PackerConfig `mapstructure:",squash"`
    26  
    27  	// If true, the script contains binary and line endings will not be
    28  	// converted from Windows to Unix-style.
    29  	Binary bool
    30  
    31  	// An inline script to execute. Multiple strings are all executed
    32  	// in the context of a single shell.
    33  	Inline []string
    34  
    35  	// The shebang value used when running inline scripts.
    36  	InlineShebang string `mapstructure:"inline_shebang"`
    37  
    38  	// The local path of the shell script to upload and execute.
    39  	Script string
    40  
    41  	// An array of multiple scripts to run.
    42  	Scripts []string
    43  
    44  	// An array of environment variables that will be injected before
    45  	// your command(s) are executed.
    46  	Vars []string `mapstructure:"environment_vars"`
    47  
    48  	// Write the Vars to a file and source them from there rather than declaring
    49  	// inline
    50  	UseEnvVarFile bool `mapstructure:"use_env_var_file"`
    51  
    52  	// The remote folder where the local shell script will be uploaded to.
    53  	// This should be set to a pre-existing directory, it defaults to /tmp
    54  	RemoteFolder string `mapstructure:"remote_folder"`
    55  
    56  	// The remote file name of the local shell script.
    57  	// This defaults to script_nnn.sh
    58  	RemoteFile string `mapstructure:"remote_file"`
    59  
    60  	// The remote path where the local shell script will be uploaded to.
    61  	// This should be set to a writable file that is in a pre-existing directory.
    62  	// This defaults to remote_folder/remote_file
    63  	RemotePath string `mapstructure:"remote_path"`
    64  
    65  	// The command used to execute the script. The '{{ .Path }}' variable
    66  	// should be used to specify where the script goes, {{ .Vars }}
    67  	// can be used to inject the environment_vars into the environment.
    68  	ExecuteCommand string `mapstructure:"execute_command"`
    69  
    70  	// The timeout for retrying to start the process. Until this timeout
    71  	// is reached, if the provisioner can't start a process, it retries.
    72  	// This can be set high to allow for reboots.
    73  	RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
    74  
    75  	// Whether to clean scripts up
    76  	SkipClean bool `mapstructure:"skip_clean"`
    77  
    78  	ExpectDisconnect bool `mapstructure:"expect_disconnect"`
    79  
    80  	startRetryTimeout time.Duration
    81  	ctx               interpolate.Context
    82  	// name of the tmp environment variable file, if UseEnvVarFile is true
    83  	envVarFile string
    84  }
    85  
    86  type Provisioner struct {
    87  	config Config
    88  }
    89  
    90  type ExecuteCommandTemplate struct {
    91  	Vars       string
    92  	EnvVarFile string
    93  	Path       string
    94  }
    95  
    96  func (p *Provisioner) Prepare(raws ...interface{}) error {
    97  	err := config.Decode(&p.config, &config.DecodeOpts{
    98  		Interpolate:        true,
    99  		InterpolateContext: &p.config.ctx,
   100  		InterpolateFilter: &interpolate.RenderFilter{
   101  			Exclude: []string{
   102  				"execute_command",
   103  			},
   104  		},
   105  	}, raws...)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	if p.config.ExecuteCommand == "" {
   111  		p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
   112  		if p.config.UseEnvVarFile == true {
   113  			p.config.ExecuteCommand = "chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}"
   114  		}
   115  	}
   116  
   117  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
   118  		p.config.Inline = nil
   119  	}
   120  
   121  	if p.config.InlineShebang == "" {
   122  		p.config.InlineShebang = "/bin/sh -e"
   123  	}
   124  
   125  	if p.config.RawStartRetryTimeout == "" {
   126  		p.config.RawStartRetryTimeout = "5m"
   127  	}
   128  
   129  	if p.config.RemoteFolder == "" {
   130  		p.config.RemoteFolder = "/tmp"
   131  	}
   132  
   133  	if p.config.RemoteFile == "" {
   134  		p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999))
   135  	}
   136  
   137  	if p.config.RemotePath == "" {
   138  		p.config.RemotePath = fmt.Sprintf(
   139  			"%s/%s", p.config.RemoteFolder, p.config.RemoteFile)
   140  	}
   141  
   142  	if p.config.Scripts == nil {
   143  		p.config.Scripts = make([]string, 0)
   144  	}
   145  
   146  	if p.config.Vars == nil {
   147  		p.config.Vars = make([]string, 0)
   148  	}
   149  
   150  	var errs *packer.MultiError
   151  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   152  		errs = packer.MultiErrorAppend(errs,
   153  			errors.New("Only one of script or scripts can be specified."))
   154  	}
   155  
   156  	if p.config.Script != "" {
   157  		p.config.Scripts = []string{p.config.Script}
   158  	}
   159  
   160  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   161  		errs = packer.MultiErrorAppend(errs,
   162  			errors.New("Either a script file or inline script must be specified."))
   163  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   164  		errs = packer.MultiErrorAppend(errs,
   165  			errors.New("Only a script file or an inline script can be specified, not both."))
   166  	}
   167  
   168  	for _, path := range p.config.Scripts {
   169  		if _, err := os.Stat(path); err != nil {
   170  			errs = packer.MultiErrorAppend(errs,
   171  				fmt.Errorf("Bad script '%s': %s", path, err))
   172  		}
   173  	}
   174  
   175  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   176  	for _, kv := range p.config.Vars {
   177  		vs := strings.SplitN(kv, "=", 2)
   178  		if len(vs) != 2 || vs[0] == "" {
   179  			errs = packer.MultiErrorAppend(errs,
   180  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   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  	if p.config.UseEnvVarFile == true {
   232  		tf, err := ioutil.TempFile("", "packer-shell-vars")
   233  		if err != nil {
   234  			return fmt.Errorf("Error preparing shell script: %s", err)
   235  		}
   236  		defer os.Remove(tf.Name())
   237  
   238  		// Write our contents to it
   239  		writer := bufio.NewWriter(tf)
   240  		if _, err := writer.WriteString(p.createEnvVarFileContent()); err != nil {
   241  			return fmt.Errorf("Error preparing shell script: %s", err)
   242  		}
   243  
   244  		if err := writer.Flush(); err != nil {
   245  			return fmt.Errorf("Error preparing shell script: %s", err)
   246  		}
   247  
   248  		p.config.envVarFile = tf.Name()
   249  		defer os.Remove(p.config.envVarFile)
   250  
   251  		// upload the var file
   252  		var cmd *packer.RemoteCmd
   253  		err = p.retryable(func() error {
   254  			if _, err := tf.Seek(0, 0); err != nil {
   255  				return err
   256  			}
   257  
   258  			var r io.Reader = tf
   259  			if !p.config.Binary {
   260  				r = &UnixReader{Reader: r}
   261  			}
   262  			remoteVFName := fmt.Sprintf("%s/%s", p.config.RemoteFolder,
   263  				fmt.Sprintf("varfile_%d.sh", rand.Intn(9999)))
   264  			if err := comm.Upload(remoteVFName, r, nil); err != nil {
   265  				return fmt.Errorf("Error uploading envVarFile: %s", err)
   266  			}
   267  			tf.Close()
   268  
   269  			cmd = &packer.RemoteCmd{
   270  				Command: fmt.Sprintf("chmod 0600 %s", remoteVFName),
   271  			}
   272  			if err := comm.Start(cmd); err != nil {
   273  				return fmt.Errorf(
   274  					"Error chmodding script file to 0600 in remote "+
   275  						"machine: %s", err)
   276  			}
   277  			cmd.Wait()
   278  			p.config.envVarFile = remoteVFName
   279  			return nil
   280  		})
   281  	}
   282  
   283  	// Create environment variables to set before executing the command
   284  	flattenedEnvVars := p.createFlattenedEnvVars()
   285  
   286  	for _, path := range scripts {
   287  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   288  
   289  		log.Printf("Opening %s for reading", path)
   290  		f, err := os.Open(path)
   291  		if err != nil {
   292  			return fmt.Errorf("Error opening shell script: %s", err)
   293  		}
   294  		defer f.Close()
   295  
   296  		// Compile the command
   297  		p.config.ctx.Data = &ExecuteCommandTemplate{
   298  			Vars:       flattenedEnvVars,
   299  			EnvVarFile: p.config.envVarFile,
   300  			Path:       p.config.RemotePath,
   301  		}
   302  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   303  		if err != nil {
   304  			return fmt.Errorf("Error processing command: %s", err)
   305  		}
   306  
   307  		// Upload the file and run the command. Do this in the context of
   308  		// a single retryable function so that we don't end up with
   309  		// the case that the upload succeeded, a restart is initiated,
   310  		// and then the command is executed but the file doesn't exist
   311  		// any longer.
   312  		var cmd *packer.RemoteCmd
   313  		err = p.retryable(func() error {
   314  			if _, err := f.Seek(0, 0); err != nil {
   315  				return err
   316  			}
   317  
   318  			var r io.Reader = f
   319  			if !p.config.Binary {
   320  				r = &UnixReader{Reader: r}
   321  			}
   322  
   323  			if err := comm.Upload(p.config.RemotePath, r, nil); err != nil {
   324  				return fmt.Errorf("Error uploading script: %s", err)
   325  			}
   326  
   327  			cmd = &packer.RemoteCmd{
   328  				Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath),
   329  			}
   330  			if err := comm.Start(cmd); err != nil {
   331  				return fmt.Errorf(
   332  					"Error chmodding script file to 0755 in remote "+
   333  						"machine: %s", err)
   334  			}
   335  			cmd.Wait()
   336  
   337  			cmd = &packer.RemoteCmd{Command: command}
   338  			return cmd.StartWithUi(comm, ui)
   339  		})
   340  
   341  		if err != nil {
   342  			return err
   343  		}
   344  
   345  		// If the exit code indicates a remote disconnect, fail unless
   346  		// we were expecting it.
   347  		if cmd.ExitStatus == packer.CmdDisconnect {
   348  			if !p.config.ExpectDisconnect {
   349  				return fmt.Errorf("Script disconnected unexpectedly. " +
   350  					"If you expected your script to disconnect, i.e. from a " +
   351  					"restart, you can try adding `\"expect_disconnect\": true` " +
   352  					"to the shell provisioner parameters.")
   353  			}
   354  		} else if cmd.ExitStatus != 0 {
   355  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   356  		}
   357  
   358  		if !p.config.SkipClean {
   359  
   360  			// Delete the temporary file we created. We retry this a few times
   361  			// since if the above rebooted we have to wait until the reboot
   362  			// completes.
   363  			err = p.cleanupRemoteFile(p.config.RemotePath, comm)
   364  			if err != nil {
   365  				return err
   366  			}
   367  			err = p.cleanupRemoteFile(p.config.envVarFile, comm)
   368  			if err != nil {
   369  				return err
   370  			}
   371  		}
   372  	}
   373  
   374  	return nil
   375  }
   376  
   377  func (p *Provisioner) cleanupRemoteFile(path string, comm packer.Communicator) error {
   378  	err := p.retryable(func() error {
   379  		cmd := &packer.RemoteCmd{
   380  			Command: fmt.Sprintf("rm -f %s", path),
   381  		}
   382  		if err := comm.Start(cmd); err != nil {
   383  			return fmt.Errorf(
   384  				"Error removing temporary script at %s: %s",
   385  				path, err)
   386  		}
   387  		cmd.Wait()
   388  		// treat disconnects as retryable by returning an error
   389  		if cmd.ExitStatus == packer.CmdDisconnect {
   390  			return fmt.Errorf("Disconnect while removing temporary script.")
   391  		}
   392  		if cmd.ExitStatus != 0 {
   393  			return fmt.Errorf(
   394  				"Error removing temporary script at %s!",
   395  				path)
   396  		}
   397  		return nil
   398  	})
   399  
   400  	if err != nil {
   401  		return err
   402  	}
   403  
   404  	return nil
   405  }
   406  
   407  func (p *Provisioner) Cancel() {
   408  	// Just hard quit. It isn't a big deal if what we're doing keeps
   409  	// running on the other side.
   410  	os.Exit(0)
   411  }
   412  
   413  // retryable will retry the given function over and over until a
   414  // non-error is returned.
   415  func (p *Provisioner) retryable(f func() error) error {
   416  	startTimeout := time.After(p.config.startRetryTimeout)
   417  	for {
   418  		var err error
   419  		if err = f(); err == nil {
   420  			return nil
   421  		}
   422  
   423  		// Create an error and log it
   424  		err = fmt.Errorf("Retryable error: %s", err)
   425  		log.Print(err.Error())
   426  
   427  		// Check if we timed out, otherwise we retry. It is safe to
   428  		// retry since the only error case above is if the command
   429  		// failed to START.
   430  		select {
   431  		case <-startTimeout:
   432  			return err
   433  		default:
   434  			time.Sleep(2 * time.Second)
   435  		}
   436  	}
   437  }
   438  
   439  func (p *Provisioner) escapeEnvVars() ([]string, map[string]string) {
   440  	envVars := make(map[string]string)
   441  
   442  	// Always available Packer provided env vars
   443  	envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName)
   444  	envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType)
   445  	httpAddr := common.GetHTTPAddr()
   446  	if httpAddr != "" {
   447  		envVars["PACKER_HTTP_ADDR"] = fmt.Sprintf("%s", httpAddr)
   448  	}
   449  
   450  	// Split vars into key/value components
   451  	for _, envVar := range p.config.Vars {
   452  		keyValue := strings.SplitN(envVar, "=", 2)
   453  		// Store pair, replacing any single quotes in value so they parse
   454  		// correctly with required environment variable format
   455  		envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
   456  	}
   457  
   458  	// Create a list of env var keys in sorted order
   459  	var keys []string
   460  	for k := range envVars {
   461  		keys = append(keys, k)
   462  	}
   463  	sort.Strings(keys)
   464  
   465  	return keys, envVars
   466  }
   467  
   468  func (p *Provisioner) createEnvVarFileContent() string {
   469  	keys, envVars := p.escapeEnvVars()
   470  
   471  	flattened := ""
   472  	// Re-assemble vars surrounding value with single quotes and flatten
   473  	for _, key := range keys {
   474  		flattened += fmt.Sprintf("export %s='%s'\n", key, envVars[key])
   475  	}
   476  
   477  	return flattened
   478  }
   479  
   480  func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
   481  	keys, envVars := p.escapeEnvVars()
   482  
   483  	// Re-assemble vars surrounding value with single quotes and flatten
   484  	for _, key := range keys {
   485  		flattened += fmt.Sprintf("%s='%s' ", key, envVars[key])
   486  	}
   487  	return
   488  }