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