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