github.com/phobos182/packer@v0.2.3-0.20130819023704-c84d2aeffc68/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  	"github.com/mitchellh/packer/common"
    10  	"github.com/mitchellh/packer/packer"
    11  	"io/ioutil"
    12  	"log"
    13  	"os"
    14  	"strings"
    15  	"time"
    16  )
    17  
    18  const DefaultRemotePath = "/tmp/script.sh"
    19  
    20  type config struct {
    21  	common.PackerConfig `mapstructure:",squash"`
    22  
    23  	// An inline script to execute. Multiple strings are all executed
    24  	// in the context of a single shell.
    25  	Inline []string
    26  
    27  	// The shebang value used when running inline scripts.
    28  	InlineShebang string `mapstructure:"inline_shebang"`
    29  
    30  	// The local path of the shell script to upload and execute.
    31  	Script string
    32  
    33  	// An array of multiple scripts to run.
    34  	Scripts []string
    35  
    36  	// An array of environment variables that will be injected before
    37  	// your command(s) are executed.
    38  	Vars []string `mapstructure:"environment_vars"`
    39  
    40  	// The remote path where the local shell script will be uploaded to.
    41  	// This should be set to a writable file that is in a pre-existing directory.
    42  	RemotePath string `mapstructure:"remote_path"`
    43  
    44  	// The command used to execute the script. The '{{ .Path }}' variable
    45  	// should be used to specify where the script goes, {{ .Vars }}
    46  	// can be used to inject the environment_vars into the environment.
    47  	ExecuteCommand string `mapstructure:"execute_command"`
    48  
    49  	// The timeout for retrying to start the process. Until this timeout
    50  	// is reached, if the provisioner can't start a process, it retries.
    51  	// This can be set high to allow for reboots.
    52  	RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
    53  
    54  	startRetryTimeout time.Duration
    55  	tpl               *packer.ConfigTemplate
    56  }
    57  
    58  type Provisioner struct {
    59  	config config
    60  }
    61  
    62  type ExecuteCommandTemplate struct {
    63  	Vars string
    64  	Path string
    65  }
    66  
    67  func (p *Provisioner) Prepare(raws ...interface{}) error {
    68  	md, err := common.DecodeConfig(&p.config, raws...)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	p.config.tpl, err = packer.NewConfigTemplate()
    74  	if err != nil {
    75  		return err
    76  	}
    77  	p.config.tpl.UserVars = p.config.PackerUserVars
    78  
    79  	// Accumulate any errors
    80  	errs := common.CheckUnusedConfig(md)
    81  
    82  	if p.config.ExecuteCommand == "" {
    83  		p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
    84  	}
    85  
    86  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
    87  		p.config.Inline = nil
    88  	}
    89  
    90  	if p.config.InlineShebang == "" {
    91  		p.config.InlineShebang = "/bin/sh"
    92  	}
    93  
    94  	if p.config.RawStartRetryTimeout == "" {
    95  		p.config.RawStartRetryTimeout = "5m"
    96  	}
    97  
    98  	if p.config.RemotePath == "" {
    99  		p.config.RemotePath = DefaultRemotePath
   100  	}
   101  
   102  	if p.config.Scripts == nil {
   103  		p.config.Scripts = make([]string, 0)
   104  	}
   105  
   106  	if p.config.Vars == nil {
   107  		p.config.Vars = make([]string, 0)
   108  	}
   109  
   110  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   111  		errs = packer.MultiErrorAppend(errs,
   112  			errors.New("Only one of script or scripts can be specified."))
   113  	}
   114  
   115  	if p.config.Script != "" {
   116  		p.config.Scripts = []string{p.config.Script}
   117  	}
   118  
   119  	templates := map[string]*string{
   120  		"inline_shebang":      &p.config.InlineShebang,
   121  		"script":              &p.config.Script,
   122  		"start_retry_timeout": &p.config.RawStartRetryTimeout,
   123  		"remote_path":         &p.config.RemotePath,
   124  	}
   125  
   126  	for n, ptr := range templates {
   127  		var err error
   128  		*ptr, err = p.config.tpl.Process(*ptr, nil)
   129  		if err != nil {
   130  			errs = packer.MultiErrorAppend(
   131  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   132  		}
   133  	}
   134  
   135  	sliceTemplates := map[string][]string{
   136  		"inline":           p.config.Inline,
   137  		"scripts":          p.config.Scripts,
   138  		"environment_vars": p.config.Vars,
   139  	}
   140  
   141  	for n, slice := range sliceTemplates {
   142  		for i, elem := range slice {
   143  			var err error
   144  			slice[i], err = p.config.tpl.Process(elem, nil)
   145  			if err != nil {
   146  				errs = packer.MultiErrorAppend(
   147  					errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
   148  			}
   149  		}
   150  	}
   151  
   152  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   153  		errs = packer.MultiErrorAppend(errs,
   154  			errors.New("Either a script file or inline script must be specified."))
   155  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   156  		errs = packer.MultiErrorAppend(errs,
   157  			errors.New("Only a script file or an inline script can be specified, not both."))
   158  	}
   159  
   160  	for _, path := range p.config.Scripts {
   161  		if _, err := os.Stat(path); err != nil {
   162  			errs = packer.MultiErrorAppend(errs,
   163  				fmt.Errorf("Bad script '%s': %s", path, err))
   164  		}
   165  	}
   166  
   167  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   168  	for _, kv := range p.config.Vars {
   169  		vs := strings.Split(kv, "=")
   170  		if len(vs) != 2 || vs[0] == "" {
   171  			errs = packer.MultiErrorAppend(errs,
   172  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   173  		}
   174  	}
   175  
   176  	if p.config.RawStartRetryTimeout != "" {
   177  		p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
   178  		if err != nil {
   179  			errs = packer.MultiErrorAppend(
   180  				errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
   181  		}
   182  	}
   183  
   184  	if errs != nil && len(errs.Errors) > 0 {
   185  		return errs
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   192  	scripts := make([]string, len(p.config.Scripts))
   193  	copy(scripts, p.config.Scripts)
   194  
   195  	// If we have an inline script, then turn that into a temporary
   196  	// shell script and use that.
   197  	if p.config.Inline != nil {
   198  		tf, err := ioutil.TempFile("", "packer-shell")
   199  		if err != nil {
   200  			return fmt.Errorf("Error preparing shell script: %s", err)
   201  		}
   202  		defer os.Remove(tf.Name())
   203  
   204  		// Set the path to the temporary file
   205  		scripts = append(scripts, tf.Name())
   206  
   207  		// Write our contents to it
   208  		writer := bufio.NewWriter(tf)
   209  		writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
   210  		for _, command := range p.config.Inline {
   211  			if _, err := writer.WriteString(command + "\n"); err != nil {
   212  				return fmt.Errorf("Error preparing shell script: %s", err)
   213  			}
   214  		}
   215  
   216  		if err := writer.Flush(); err != nil {
   217  			return fmt.Errorf("Error preparing shell script: %s", err)
   218  		}
   219  
   220  		tf.Close()
   221  	}
   222  
   223  	// Build our variables up by adding in the build name and builder type
   224  	envVars := make([]string, len(p.config.Vars)+2)
   225  	envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName
   226  	envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType
   227  	copy(envVars[2:], p.config.Vars)
   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  		log.Printf("Uploading %s => %s", path, p.config.RemotePath)
   240  		err = comm.Upload(p.config.RemotePath, f)
   241  		if err != nil {
   242  			return fmt.Errorf("Error uploading shell script: %s", err)
   243  		}
   244  
   245  		// Close the original file since we copied it
   246  		f.Close()
   247  
   248  		// Flatten the environment variables
   249  		flattendVars := strings.Join(envVars, " ")
   250  
   251  		// Compile the command
   252  		command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{
   253  			Vars: flattendVars,
   254  			Path: p.config.RemotePath,
   255  		})
   256  		if err != nil {
   257  			return fmt.Errorf("Error processing command: %s", err)
   258  		}
   259  
   260  		cmd := &packer.RemoteCmd{Command: command}
   261  		startTimeout := time.After(p.config.startRetryTimeout)
   262  		log.Printf("Executing command: %s", cmd.Command)
   263  		for {
   264  			if err := cmd.StartWithUi(comm, ui); err == nil {
   265  				break
   266  			}
   267  
   268  			// Create an error and log it
   269  			err = fmt.Errorf("Error executing command: %s", err)
   270  			log.Printf(err.Error())
   271  
   272  			// Check if we timed out, otherwise we retry. It is safe to
   273  			// retry since the only error case above is if the command
   274  			// failed to START.
   275  			select {
   276  			case <-startTimeout:
   277  				return err
   278  			default:
   279  				time.Sleep(2 * time.Second)
   280  			}
   281  		}
   282  
   283  		if cmd.ExitStatus != 0 {
   284  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   285  		}
   286  	}
   287  
   288  	return nil
   289  }