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