github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/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  	ExpectDisconnect *bool `mapstructure:"expect_disconnect"`
    74  
    75  	startRetryTimeout time.Duration
    76  	ctx               interpolate.Context
    77  }
    78  
    79  type Provisioner struct {
    80  	config Config
    81  }
    82  
    83  type ExecuteCommandTemplate struct {
    84  	Vars string
    85  	Path string
    86  }
    87  
    88  func (p *Provisioner) Prepare(raws ...interface{}) error {
    89  	err := config.Decode(&p.config, &config.DecodeOpts{
    90  		Interpolate:        true,
    91  		InterpolateContext: &p.config.ctx,
    92  		InterpolateFilter: &interpolate.RenderFilter{
    93  			Exclude: []string{
    94  				"execute_command",
    95  			},
    96  		},
    97  	}, raws...)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	if p.config.ExecuteCommand == "" {
   103  		p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
   104  	}
   105  
   106  	if p.config.ExpectDisconnect == nil {
   107  		t := true
   108  		p.config.ExpectDisconnect = &t
   109  	}
   110  
   111  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
   112  		p.config.Inline = nil
   113  	}
   114  
   115  	if p.config.InlineShebang == "" {
   116  		p.config.InlineShebang = "/bin/sh -e"
   117  	}
   118  
   119  	if p.config.RawStartRetryTimeout == "" {
   120  		p.config.RawStartRetryTimeout = "5m"
   121  	}
   122  
   123  	if p.config.RemoteFolder == "" {
   124  		p.config.RemoteFolder = "/tmp"
   125  	}
   126  
   127  	if p.config.RemoteFile == "" {
   128  		p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999))
   129  	}
   130  
   131  	if p.config.RemotePath == "" {
   132  		p.config.RemotePath = fmt.Sprintf(
   133  			"%s/%s", p.config.RemoteFolder, p.config.RemoteFile)
   134  	}
   135  
   136  	if p.config.Scripts == nil {
   137  		p.config.Scripts = make([]string, 0)
   138  	}
   139  
   140  	if p.config.Vars == nil {
   141  		p.config.Vars = make([]string, 0)
   142  	}
   143  
   144  	var errs *packer.MultiError
   145  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   146  		errs = packer.MultiErrorAppend(errs,
   147  			errors.New("Only one of script or scripts can be specified."))
   148  	}
   149  
   150  	if p.config.Script != "" {
   151  		p.config.Scripts = []string{p.config.Script}
   152  	}
   153  
   154  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   155  		errs = packer.MultiErrorAppend(errs,
   156  			errors.New("Either a script file or inline script must be specified."))
   157  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   158  		errs = packer.MultiErrorAppend(errs,
   159  			errors.New("Only a script file or an inline script can be specified, not both."))
   160  	}
   161  
   162  	for _, path := range p.config.Scripts {
   163  		if _, err := os.Stat(path); err != nil {
   164  			errs = packer.MultiErrorAppend(errs,
   165  				fmt.Errorf("Bad script '%s': %s", path, err))
   166  		}
   167  	}
   168  
   169  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   170  	for idx, kv := range p.config.Vars {
   171  		vs := strings.SplitN(kv, "=", 2)
   172  		if len(vs) != 2 || vs[0] == "" {
   173  			errs = packer.MultiErrorAppend(errs,
   174  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   175  		} else {
   176  			// Replace single quotes so they parse
   177  			vs[1] = strings.Replace(vs[1], "'", `'"'"'`, -1)
   178  
   179  			// Single quote env var values
   180  			p.config.Vars[idx] = fmt.Sprintf("%s='%s'", vs[0], vs[1])
   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  	// Build our variables up by adding in the build name and builder type
   232  	envVars := make([]string, len(p.config.Vars)+2)
   233  	envVars[0] = fmt.Sprintf("PACKER_BUILD_NAME='%s'", p.config.PackerBuildName)
   234  	envVars[1] = fmt.Sprintf("PACKER_BUILDER_TYPE='%s'", p.config.PackerBuilderType)
   235  	copy(envVars[2:], p.config.Vars)
   236  
   237  	for _, path := range scripts {
   238  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   239  
   240  		log.Printf("Opening %s for reading", path)
   241  		f, err := os.Open(path)
   242  		if err != nil {
   243  			return fmt.Errorf("Error opening shell script: %s", err)
   244  		}
   245  		defer f.Close()
   246  
   247  		// Flatten the environment variables
   248  		flattendVars := strings.Join(envVars, " ")
   249  
   250  		// Compile the command
   251  		p.config.ctx.Data = &ExecuteCommandTemplate{
   252  			Vars: flattendVars,
   253  			Path: p.config.RemotePath,
   254  		}
   255  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   256  		if err != nil {
   257  			return fmt.Errorf("Error processing command: %s", err)
   258  		}
   259  
   260  		// Upload the file and run the command. Do this in the context of
   261  		// a single retryable function so that we don't end up with
   262  		// the case that the upload succeeded, a restart is initiated,
   263  		// and then the command is executed but the file doesn't exist
   264  		// any longer.
   265  		var cmd *packer.RemoteCmd
   266  		err = p.retryable(func() error {
   267  			if _, err := f.Seek(0, 0); err != nil {
   268  				return err
   269  			}
   270  
   271  			var r io.Reader = f
   272  			if !p.config.Binary {
   273  				r = &UnixReader{Reader: r}
   274  			}
   275  
   276  			if err := comm.Upload(p.config.RemotePath, r, nil); err != nil {
   277  				return fmt.Errorf("Error uploading script: %s", err)
   278  			}
   279  
   280  			cmd = &packer.RemoteCmd{
   281  				Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath),
   282  			}
   283  			if err := comm.Start(cmd); err != nil {
   284  				return fmt.Errorf(
   285  					"Error chmodding script file to 0755 in remote "+
   286  						"machine: %s", err)
   287  			}
   288  			cmd.Wait()
   289  
   290  			cmd = &packer.RemoteCmd{Command: command}
   291  			return cmd.StartWithUi(comm, ui)
   292  		})
   293  
   294  		if err != nil {
   295  			return err
   296  		}
   297  
   298  		// If the exit code indicates a remote disconnect, fail unless
   299  		// we were expecting it.
   300  		if cmd.ExitStatus == packer.CmdDisconnect {
   301  			if !*p.config.ExpectDisconnect {
   302  				return fmt.Errorf("Script disconnected unexpectedly.")
   303  			}
   304  		} else if cmd.ExitStatus != 0 {
   305  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   306  		}
   307  
   308  		if !p.config.SkipClean {
   309  
   310  			// Delete the temporary file we created. We retry this a few times
   311  			// since if the above rebooted we have to wait until the reboot
   312  			// completes.
   313  			err = p.retryable(func() error {
   314  				cmd = &packer.RemoteCmd{
   315  					Command: fmt.Sprintf("rm -f %s", p.config.RemotePath),
   316  				}
   317  				if err := comm.Start(cmd); err != nil {
   318  					return fmt.Errorf(
   319  						"Error removing temporary script at %s: %s",
   320  						p.config.RemotePath, err)
   321  				}
   322  				cmd.Wait()
   323  				return nil
   324  			})
   325  			if err != nil {
   326  				return err
   327  			}
   328  
   329  			if cmd.ExitStatus != 0 {
   330  				return fmt.Errorf(
   331  					"Error removing temporary script at %s!",
   332  					p.config.RemotePath)
   333  			}
   334  		}
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  func (p *Provisioner) Cancel() {
   341  	// Just hard quit. It isn't a big deal if what we're doing keeps
   342  	// running on the other side.
   343  	os.Exit(0)
   344  }
   345  
   346  // retryable will retry the given function over and over until a
   347  // non-error is returned.
   348  func (p *Provisioner) retryable(f func() error) error {
   349  	startTimeout := time.After(p.config.startRetryTimeout)
   350  	for {
   351  		var err error
   352  		if err = f(); err == nil {
   353  			return nil
   354  		}
   355  
   356  		// Create an error and log it
   357  		err = fmt.Errorf("Retryable error: %s", err)
   358  		log.Printf(err.Error())
   359  
   360  		// Check if we timed out, otherwise we retry. It is safe to
   361  		// retry since the only error case above is if the command
   362  		// failed to START.
   363  		select {
   364  		case <-startTimeout:
   365  			return err
   366  		default:
   367  			time.Sleep(2 * time.Second)
   368  		}
   369  	}
   370  }