github.com/aclaygray/packer@v1.3.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/hashicorp/packer/common"
    17  	"github.com/hashicorp/packer/helper/config"
    18  	"github.com/hashicorp/packer/packer"
    19  	"github.com/hashicorp/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  	return errs
   154  }
   155  
   156  // This function takes the inline scripts, concatenates them
   157  // into a temporary file and returns a string containing the location
   158  // of said file.
   159  func extractScript(p *Provisioner) (string, error) {
   160  	temp, err := ioutil.TempFile(os.TempDir(), "packer-windows-shell-provisioner")
   161  	if err != nil {
   162  		log.Printf("Unable to create temporary file for inline scripts: %s", err)
   163  		return "", err
   164  	}
   165  	writer := bufio.NewWriter(temp)
   166  	for _, command := range p.config.Inline {
   167  		log.Printf("Found command: %s", command)
   168  		if _, err := writer.WriteString(command + "\n"); err != nil {
   169  			return "", fmt.Errorf("Error preparing shell script: %s", err)
   170  		}
   171  	}
   172  
   173  	if err := writer.Flush(); err != nil {
   174  		return "", fmt.Errorf("Error preparing shell script: %s", err)
   175  	}
   176  
   177  	temp.Close()
   178  
   179  	return temp.Name(), nil
   180  }
   181  
   182  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   183  	ui.Say(fmt.Sprintf("Provisioning with windows-shell..."))
   184  	scripts := make([]string, len(p.config.Scripts))
   185  	copy(scripts, p.config.Scripts)
   186  
   187  	if p.config.Inline != nil {
   188  		temp, err := extractScript(p)
   189  		if err != nil {
   190  			ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err))
   191  		}
   192  		scripts = append(scripts, temp)
   193  		// Remove temp script containing the inline commands when done
   194  		defer os.Remove(temp)
   195  	}
   196  
   197  	for _, path := range scripts {
   198  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   199  
   200  		log.Printf("Opening %s for reading", path)
   201  		f, err := os.Open(path)
   202  		if err != nil {
   203  			return fmt.Errorf("Error opening shell script: %s", err)
   204  		}
   205  		defer f.Close()
   206  
   207  		// Create environment variables to set before executing the command
   208  		flattenedVars := p.createFlattenedEnvVars()
   209  
   210  		// Compile the command
   211  		p.config.ctx.Data = &ExecuteCommandTemplate{
   212  			Vars: flattenedVars,
   213  			Path: p.config.RemotePath,
   214  		}
   215  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   216  		if err != nil {
   217  			return fmt.Errorf("Error processing command: %s", err)
   218  		}
   219  
   220  		// Upload the file and run the command. Do this in the context of
   221  		// a single retryable function so that we don't end up with
   222  		// the case that the upload succeeded, a restart is initiated,
   223  		// and then the command is executed but the file doesn't exist
   224  		// any longer.
   225  		var cmd *packer.RemoteCmd
   226  		err = p.retryable(func() error {
   227  			if _, err := f.Seek(0, 0); err != nil {
   228  				return err
   229  			}
   230  
   231  			if err := comm.Upload(p.config.RemotePath, f, nil); err != nil {
   232  				return fmt.Errorf("Error uploading script: %s", err)
   233  			}
   234  
   235  			cmd = &packer.RemoteCmd{Command: command}
   236  			return cmd.StartWithUi(comm, ui)
   237  		})
   238  		if err != nil {
   239  			return err
   240  		}
   241  
   242  		// Close the original file since we copied it
   243  		f.Close()
   244  
   245  		if cmd.ExitStatus != 0 {
   246  			return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   247  		}
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  func (p *Provisioner) Cancel() {
   254  	// Just hard quit. It isn't a big deal if what we're doing keeps
   255  	// running on the other side.
   256  	os.Exit(0)
   257  }
   258  
   259  // retryable will retry the given function over and over until a
   260  // non-error is returned.
   261  func (p *Provisioner) retryable(f func() error) error {
   262  	startTimeout := time.After(p.config.StartRetryTimeout)
   263  	for {
   264  		var err error
   265  		if err = f(); err == nil {
   266  			return nil
   267  		}
   268  
   269  		// Create an error and log it
   270  		err = fmt.Errorf("Retryable error: %s", err)
   271  		log.Print(err.Error())
   272  
   273  		// Check if we timed out, otherwise we retry. It is safe to
   274  		// retry since the only error case above is if the command
   275  		// failed to START.
   276  		select {
   277  		case <-startTimeout:
   278  			return err
   279  		default:
   280  			time.Sleep(retryableSleep)
   281  		}
   282  	}
   283  }
   284  
   285  func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
   286  	flattened = ""
   287  	envVars := make(map[string]string)
   288  
   289  	// Always available Packer provided env vars
   290  	envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName
   291  	envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType
   292  	httpAddr := common.GetHTTPAddr()
   293  	if httpAddr != "" {
   294  		envVars["PACKER_HTTP_ADDR"] = httpAddr
   295  	}
   296  
   297  	// Split vars into key/value components
   298  	for _, envVar := range p.config.Vars {
   299  		keyValue := strings.SplitN(envVar, "=", 2)
   300  		envVars[keyValue[0]] = keyValue[1]
   301  	}
   302  	// Create a list of env var keys in sorted order
   303  	var keys []string
   304  	for k := range envVars {
   305  		keys = append(keys, k)
   306  	}
   307  	sort.Strings(keys)
   308  	// Re-assemble vars using OS specific format pattern and flatten
   309  	for _, key := range keys {
   310  		flattened += fmt.Sprintf(p.config.EnvVarFormat, key, envVars[key])
   311  	}
   312  	return
   313  }