github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/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  	// The remote folder where the local shell script will be uploaded to.
    49  	// This should be set to a pre-existing directory, it defaults to /tmp
    50  	RemoteFolder string `mapstructure:"remote_folder"`
    51  
    52  	// The remote file name of the local shell script.
    53  	// This defaults to script_nnn.sh
    54  	RemoteFile string `mapstructure:"remote_file"`
    55  
    56  	// The remote path where the local shell script will be uploaded to.
    57  	// This should be set to a writable file that is in a pre-existing directory.
    58  	// This defaults to remote_folder/remote_file
    59  	RemotePath string `mapstructure:"remote_path"`
    60  
    61  	// The command used to execute the script. The '{{ .Path }}' variable
    62  	// should be used to specify where the script goes, {{ .Vars }}
    63  	// can be used to inject the environment_vars into the environment.
    64  	ExecuteCommand string `mapstructure:"execute_command"`
    65  
    66  	// The timeout for retrying to start the process. Until this timeout
    67  	// is reached, if the provisioner can't start a process, it retries.
    68  	// This can be set high to allow for reboots.
    69  	RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
    70  
    71  	// Whether to clean scripts up
    72  	SkipClean bool `mapstructure:"skip_clean"`
    73  
    74  	ExpectDisconnect bool `mapstructure:"expect_disconnect"`
    75  
    76  	startRetryTimeout time.Duration
    77  	ctx               interpolate.Context
    78  }
    79  
    80  type Provisioner struct {
    81  	config Config
    82  }
    83  
    84  type ExecuteCommandTemplate struct {
    85  	Vars string
    86  	Path string
    87  }
    88  
    89  func (p *Provisioner) Prepare(raws ...interface{}) error {
    90  	err := config.Decode(&p.config, &config.DecodeOpts{
    91  		Interpolate:        true,
    92  		InterpolateContext: &p.config.ctx,
    93  		InterpolateFilter: &interpolate.RenderFilter{
    94  			Exclude: []string{
    95  				"execute_command",
    96  			},
    97  		},
    98  	}, raws...)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	if p.config.ExecuteCommand == "" {
   104  		p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
   105  	}
   106  
   107  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
   108  		p.config.Inline = nil
   109  	}
   110  
   111  	if p.config.InlineShebang == "" {
   112  		p.config.InlineShebang = "/bin/sh -e"
   113  	}
   114  
   115  	if p.config.RawStartRetryTimeout == "" {
   116  		p.config.RawStartRetryTimeout = "5m"
   117  	}
   118  
   119  	if p.config.RemoteFolder == "" {
   120  		p.config.RemoteFolder = "/tmp"
   121  	}
   122  
   123  	if p.config.RemoteFile == "" {
   124  		p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999))
   125  	}
   126  
   127  	if p.config.RemotePath == "" {
   128  		p.config.RemotePath = fmt.Sprintf(
   129  			"%s/%s", p.config.RemoteFolder, p.config.RemoteFile)
   130  	}
   131  
   132  	if p.config.Scripts == nil {
   133  		p.config.Scripts = make([]string, 0)
   134  	}
   135  
   136  	if p.config.Vars == nil {
   137  		p.config.Vars = make([]string, 0)
   138  	}
   139  
   140  	var errs *packer.MultiError
   141  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   142  		errs = packer.MultiErrorAppend(errs,
   143  			errors.New("Only one of script or scripts can be specified."))
   144  	}
   145  
   146  	if p.config.Script != "" {
   147  		p.config.Scripts = []string{p.config.Script}
   148  	}
   149  
   150  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   151  		errs = packer.MultiErrorAppend(errs,
   152  			errors.New("Either a script file or inline script must be specified."))
   153  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   154  		errs = packer.MultiErrorAppend(errs,
   155  			errors.New("Only a script file or an inline script can be specified, not both."))
   156  	}
   157  
   158  	for _, path := range p.config.Scripts {
   159  		if _, err := os.Stat(path); err != nil {
   160  			errs = packer.MultiErrorAppend(errs,
   161  				fmt.Errorf("Bad script '%s': %s", path, err))
   162  		}
   163  	}
   164  
   165  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   166  	for _, kv := range p.config.Vars {
   167  		vs := strings.SplitN(kv, "=", 2)
   168  		if len(vs) != 2 || vs[0] == "" {
   169  			errs = packer.MultiErrorAppend(errs,
   170  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   171  		}
   172  	}
   173  
   174  	if p.config.RawStartRetryTimeout != "" {
   175  		p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
   176  		if err != nil {
   177  			errs = packer.MultiErrorAppend(
   178  				errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
   179  		}
   180  	}
   181  
   182  	if errs != nil && len(errs.Errors) > 0 {
   183  		return errs
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   190  	scripts := make([]string, len(p.config.Scripts))
   191  	copy(scripts, p.config.Scripts)
   192  
   193  	// If we have an inline script, then turn that into a temporary
   194  	// shell script and use that.
   195  	if p.config.Inline != nil {
   196  		tf, err := ioutil.TempFile("", "packer-shell")
   197  		if err != nil {
   198  			return fmt.Errorf("Error preparing shell script: %s", err)
   199  		}
   200  		defer os.Remove(tf.Name())
   201  
   202  		// Set the path to the temporary file
   203  		scripts = append(scripts, tf.Name())
   204  
   205  		// Write our contents to it
   206  		writer := bufio.NewWriter(tf)
   207  		writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
   208  		for _, command := range p.config.Inline {
   209  			if _, err := writer.WriteString(command + "\n"); err != nil {
   210  				return fmt.Errorf("Error preparing shell script: %s", err)
   211  			}
   212  		}
   213  
   214  		if err := writer.Flush(); err != nil {
   215  			return fmt.Errorf("Error preparing shell script: %s", err)
   216  		}
   217  
   218  		tf.Close()
   219  	}
   220  
   221  	// Create environment variables to set before executing the command
   222  	flattenedEnvVars := p.createFlattenedEnvVars()
   223  
   224  	for _, path := range scripts {
   225  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   226  
   227  		log.Printf("Opening %s for reading", path)
   228  		f, err := os.Open(path)
   229  		if err != nil {
   230  			return fmt.Errorf("Error opening shell script: %s", err)
   231  		}
   232  		defer f.Close()
   233  
   234  		// Compile the command
   235  		p.config.ctx.Data = &ExecuteCommandTemplate{
   236  			Vars: flattenedEnvVars,
   237  			Path: p.config.RemotePath,
   238  		}
   239  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   240  		if err != nil {
   241  			return fmt.Errorf("Error processing command: %s", err)
   242  		}
   243  
   244  		// Upload the file and run the command. Do this in the context of
   245  		// a single retryable function so that we don't end up with
   246  		// the case that the upload succeeded, a restart is initiated,
   247  		// and then the command is executed but the file doesn't exist
   248  		// any longer.
   249  		var cmd *packer.RemoteCmd
   250  		err = p.retryable(func() error {
   251  			if _, err := f.Seek(0, 0); err != nil {
   252  				return err
   253  			}
   254  
   255  			var r io.Reader = f
   256  			if !p.config.Binary {
   257  				r = &UnixReader{Reader: r}
   258  			}
   259  
   260  			if err := comm.Upload(p.config.RemotePath, r, nil); err != nil {
   261  				return fmt.Errorf("Error uploading script: %s", err)
   262  			}
   263  
   264  			cmd = &packer.RemoteCmd{
   265  				Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath),
   266  			}
   267  			if err := comm.Start(cmd); err != nil {
   268  				return fmt.Errorf(
   269  					"Error chmodding script file to 0755 in remote "+
   270  						"machine: %s", err)
   271  			}
   272  			cmd.Wait()
   273  
   274  			cmd = &packer.RemoteCmd{Command: command}
   275  			return cmd.StartWithUi(comm, ui)
   276  		})
   277  
   278  		if err != nil {
   279  			return err
   280  		}
   281  
   282  		// If the exit code indicates a remote disconnect, fail unless
   283  		// we were expecting it.
   284  		if cmd.ExitStatus == packer.CmdDisconnect {
   285  			if !p.config.ExpectDisconnect {
   286  				return fmt.Errorf("Script disconnected unexpectedly. " +
   287  					"If you expected your script to disconnect, i.e. from a " +
   288  					"restart, you can try adding `\"expect_disconnect\": true` " +
   289  					"to the shell provisioner parameters.")
   290  			}
   291  		} else if cmd.ExitStatus != 0 {
   292  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   293  		}
   294  
   295  		if !p.config.SkipClean {
   296  
   297  			// Delete the temporary file we created. We retry this a few times
   298  			// since if the above rebooted we have to wait until the reboot
   299  			// completes.
   300  			err = p.retryable(func() error {
   301  				cmd = &packer.RemoteCmd{
   302  					Command: fmt.Sprintf("rm -f %s", p.config.RemotePath),
   303  				}
   304  				if err := comm.Start(cmd); err != nil {
   305  					return fmt.Errorf(
   306  						"Error removing temporary script at %s: %s",
   307  						p.config.RemotePath, err)
   308  				}
   309  				cmd.Wait()
   310  				// treat disconnects as retryable by returning an error
   311  				if cmd.ExitStatus == packer.CmdDisconnect {
   312  					return fmt.Errorf("Disconnect while removing temporary script.")
   313  				}
   314  				return nil
   315  			})
   316  			if err != nil {
   317  				return err
   318  			}
   319  
   320  			if cmd.ExitStatus != 0 {
   321  				return fmt.Errorf(
   322  					"Error removing temporary script at %s!",
   323  					p.config.RemotePath)
   324  			}
   325  		}
   326  	}
   327  
   328  	return nil
   329  }
   330  
   331  func (p *Provisioner) Cancel() {
   332  	// Just hard quit. It isn't a big deal if what we're doing keeps
   333  	// running on the other side.
   334  	os.Exit(0)
   335  }
   336  
   337  // retryable will retry the given function over and over until a
   338  // non-error is returned.
   339  func (p *Provisioner) retryable(f func() error) error {
   340  	startTimeout := time.After(p.config.startRetryTimeout)
   341  	for {
   342  		var err error
   343  		if err = f(); err == nil {
   344  			return nil
   345  		}
   346  
   347  		// Create an error and log it
   348  		err = fmt.Errorf("Retryable error: %s", err)
   349  		log.Print(err.Error())
   350  
   351  		// Check if we timed out, otherwise we retry. It is safe to
   352  		// retry since the only error case above is if the command
   353  		// failed to START.
   354  		select {
   355  		case <-startTimeout:
   356  			return err
   357  		default:
   358  			time.Sleep(2 * time.Second)
   359  		}
   360  	}
   361  }
   362  
   363  func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
   364  	flattened = ""
   365  	envVars := make(map[string]string)
   366  
   367  	// Always available Packer provided env vars
   368  	envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName)
   369  	envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType)
   370  	httpAddr := common.GetHTTPAddr()
   371  	if httpAddr != "" {
   372  		envVars["PACKER_HTTP_ADDR"] = fmt.Sprintf("%s", httpAddr)
   373  	}
   374  
   375  	// Split vars into key/value components
   376  	for _, envVar := range p.config.Vars {
   377  		keyValue := strings.SplitN(envVar, "=", 2)
   378  		// Store pair, replacing any single quotes in value so they parse
   379  		// correctly with required environment variable format
   380  		envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
   381  	}
   382  
   383  	// Create a list of env var keys in sorted order
   384  	var keys []string
   385  	for k := range envVars {
   386  		keys = append(keys, k)
   387  	}
   388  	sort.Strings(keys)
   389  
   390  	// Re-assemble vars surrounding value with single quotes and flatten
   391  	for _, key := range keys {
   392  		flattened += fmt.Sprintf("%s='%s' ", key, envVars[key])
   393  	}
   394  	return
   395  }