github.com/dacamp/packer@v0.10.2/provisioner/windows-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/ioutil"
    10  	"log"
    11  	"os"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/mitchellh/packer/common"
    17  	"github.com/mitchellh/packer/helper/config"
    18  	"github.com/mitchellh/packer/packer"
    19  	"github.com/mitchellh/packer/template/interpolate"
    20  )
    21  
    22  const DefaultRemotePath = "c:/Windows/Temp/script.bat"
    23  
    24  var retryableSleep = 2 * time.Second
    25  
    26  type Config struct {
    27  	common.PackerConfig `mapstructure:",squash"`
    28  
    29  	// If true, the script contains binary and line endings will not be
    30  	// converted from Windows to Unix-style.
    31  	Binary bool
    32  
    33  	// An inline script to execute. Multiple strings are all executed
    34  	// in the context of a single shell.
    35  	Inline []string
    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  	StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"`
    60  
    61  	// This is used in the template generation to format environment variables
    62  	// inside the `ExecuteCommand` template.
    63  	EnvVarFormat string
    64  
    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.EnvVarFormat == "" {
    92  		p.config.EnvVarFormat = `set "%s=%s" && `
    93  	}
    94  
    95  	if p.config.ExecuteCommand == "" {
    96  		p.config.ExecuteCommand = `{{.Vars}}"{{.Path}}"`
    97  	}
    98  
    99  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
   100  		p.config.Inline = nil
   101  	}
   102  
   103  	if p.config.StartRetryTimeout == 0 {
   104  		p.config.StartRetryTimeout = 5 * time.Minute
   105  	}
   106  
   107  	if p.config.RemotePath == "" {
   108  		p.config.RemotePath = DefaultRemotePath
   109  	}
   110  
   111  	if p.config.Scripts == nil {
   112  		p.config.Scripts = make([]string, 0)
   113  	}
   114  
   115  	if p.config.Vars == nil {
   116  		p.config.Vars = make([]string, 0)
   117  	}
   118  
   119  	var errs error
   120  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   121  		errs = packer.MultiErrorAppend(errs,
   122  			errors.New("Only one of script or scripts can be specified."))
   123  	}
   124  
   125  	if p.config.Script != "" {
   126  		p.config.Scripts = []string{p.config.Script}
   127  	}
   128  
   129  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   130  		errs = packer.MultiErrorAppend(errs,
   131  			errors.New("Either a script file or inline script must be specified."))
   132  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   133  		errs = packer.MultiErrorAppend(errs,
   134  			errors.New("Only a script file or an inline script can be specified, not both."))
   135  	}
   136  
   137  	for _, path := range p.config.Scripts {
   138  		if _, err := os.Stat(path); err != nil {
   139  			errs = packer.MultiErrorAppend(errs,
   140  				fmt.Errorf("Bad script '%s': %s", path, err))
   141  		}
   142  	}
   143  
   144  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   145  	for _, kv := range p.config.Vars {
   146  		vs := strings.SplitN(kv, "=", 2)
   147  		if len(vs) != 2 || vs[0] == "" {
   148  			errs = packer.MultiErrorAppend(errs,
   149  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   150  		}
   151  	}
   152  
   153  	if errs != nil {
   154  		return errs
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // This function takes the inline scripts, concatenates them
   161  // into a temporary file and returns a string containing the location
   162  // of said file.
   163  func extractScript(p *Provisioner) (string, error) {
   164  	temp, err := ioutil.TempFile(os.TempDir(), "packer-windows-shell-provisioner")
   165  	if err != nil {
   166  		log.Printf("Unable to create temporary file for inline scripts: %s", err)
   167  		return "", err
   168  	}
   169  	writer := bufio.NewWriter(temp)
   170  	for _, command := range p.config.Inline {
   171  		log.Printf("Found command: %s", command)
   172  		if _, err := writer.WriteString(command + "\n"); err != nil {
   173  			return "", fmt.Errorf("Error preparing shell script: %s", err)
   174  		}
   175  	}
   176  
   177  	if err := writer.Flush(); err != nil {
   178  		return "", fmt.Errorf("Error preparing shell script: %s", err)
   179  	}
   180  
   181  	temp.Close()
   182  
   183  	return temp.Name(), nil
   184  }
   185  
   186  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   187  	ui.Say(fmt.Sprintf("Provisioning with windows-shell..."))
   188  	scripts := make([]string, len(p.config.Scripts))
   189  	copy(scripts, p.config.Scripts)
   190  
   191  	// Build our variables up by adding in the build name and builder type
   192  	envVars := make([]string, len(p.config.Vars)+2)
   193  	envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName
   194  	envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType
   195  
   196  	copy(envVars, p.config.Vars)
   197  
   198  	if p.config.Inline != nil {
   199  		temp, err := extractScript(p)
   200  		if err != nil {
   201  			ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err))
   202  		}
   203  		scripts = append(scripts, temp)
   204  	}
   205  
   206  	for _, path := range scripts {
   207  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   208  
   209  		log.Printf("Opening %s for reading", path)
   210  		f, err := os.Open(path)
   211  		if err != nil {
   212  			return fmt.Errorf("Error opening shell script: %s", err)
   213  		}
   214  		defer f.Close()
   215  
   216  		// Create environment variables to set before executing the command
   217  		flattendVars, err := p.createFlattenedEnvVars()
   218  		if err != nil {
   219  			return err
   220  		}
   221  
   222  		// Compile the command
   223  		p.config.ctx.Data = &ExecuteCommandTemplate{
   224  			Vars: flattendVars,
   225  			Path: p.config.RemotePath,
   226  		}
   227  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   228  		if err != nil {
   229  			return fmt.Errorf("Error processing command: %s", err)
   230  		}
   231  
   232  		// Upload the file and run the command. Do this in the context of
   233  		// a single retryable function so that we don't end up with
   234  		// the case that the upload succeeded, a restart is initiated,
   235  		// and then the command is executed but the file doesn't exist
   236  		// any longer.
   237  		var cmd *packer.RemoteCmd
   238  		err = p.retryable(func() error {
   239  			if _, err := f.Seek(0, 0); err != nil {
   240  				return err
   241  			}
   242  
   243  			if err := comm.Upload(p.config.RemotePath, f, nil); err != nil {
   244  				return fmt.Errorf("Error uploading script: %s", err)
   245  			}
   246  
   247  			cmd = &packer.RemoteCmd{Command: command}
   248  			return cmd.StartWithUi(comm, ui)
   249  		})
   250  		if err != nil {
   251  			return err
   252  		}
   253  
   254  		// Close the original file since we copied it
   255  		f.Close()
   256  
   257  		if cmd.ExitStatus != 0 {
   258  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   259  		}
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (p *Provisioner) Cancel() {
   266  	// Just hard quit. It isn't a big deal if what we're doing keeps
   267  	// running on the other side.
   268  	os.Exit(0)
   269  }
   270  
   271  // retryable will retry the given function over and over until a
   272  // non-error is returned.
   273  func (p *Provisioner) retryable(f func() error) error {
   274  	startTimeout := time.After(p.config.StartRetryTimeout)
   275  	for {
   276  		var err error
   277  		if err = f(); err == nil {
   278  			return nil
   279  		}
   280  
   281  		// Create an error and log it
   282  		err = fmt.Errorf("Retryable error: %s", err)
   283  		log.Printf(err.Error())
   284  
   285  		// Check if we timed out, otherwise we retry. It is safe to
   286  		// retry since the only error case above is if the command
   287  		// failed to START.
   288  		select {
   289  		case <-startTimeout:
   290  			return err
   291  		default:
   292  			time.Sleep(retryableSleep)
   293  		}
   294  	}
   295  }
   296  
   297  func (p *Provisioner) createFlattenedEnvVars() (flattened string, err error) {
   298  	flattened = ""
   299  	envVars := make(map[string]string)
   300  
   301  	// Always available Packer provided env vars
   302  	envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName
   303  	envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType
   304  
   305  	// Split vars into key/value components
   306  	for _, envVar := range p.config.Vars {
   307  		keyValue := strings.Split(envVar, "=")
   308  		if len(keyValue) != 2 {
   309  			err = errors.New("Shell provisioner environment variables must be in key=value format")
   310  			return
   311  		}
   312  		envVars[keyValue[0]] = keyValue[1]
   313  	}
   314  	// Create a list of env var keys in sorted order
   315  	var keys []string
   316  	for k := range envVars {
   317  		keys = append(keys, k)
   318  	}
   319  	sort.Strings(keys)
   320  	// Re-assemble vars using OS specific format pattern and flatten
   321  	for _, key := range keys {
   322  		flattened += fmt.Sprintf(p.config.EnvVarFormat, key, envVars[key])
   323  	}
   324  	return
   325  }